In the previous article, we broke our fictional 401(k) platform into potential bounded contexts.
That was the easy part.
The harder question is: what happens next?
A retirement recordkeeping platform cannot operate as a collection of completely isolated domains. Contribution Processing needs information about participants and plans. Recordkeeping needs to know when a contribution has been accepted. Investment Management provides values that affect participant accounts, and Distribution Management needs to know what money is available for withdrawal.
Even if each bounded context owns its own isolated domain model, those models still need to communicate.
This is where Context Mapping comes in.
Context Mapping makes the relationships between bounded contexts explicit. Instead of simply drawing boxes and connecting them with generic arrows, we ask more useful questions:
- Dependency: Which context relies on another to fulfill its job?
- Ownership: Who owns the model and defines the integration contract?
- Language: Is one team forced to adopt another team’s terminology?
- Protection: Where should translation happen to shield a clean domain model from external or legacy systems?
These questions matter because integration is usually where clean domain models start to decay.
Our 401(k) Platform Boundaries
In Part 3, we identified several bounded contexts:
Plaintext
┌───────────────────────────┐
│ Plan Administration │
└─────────────┬─────────────┘
│
▼
┌───────────────────────────┐
│ Participant Management │
└─────────────┬─────────────┘
│
▼
┌───────────────────────────┐
│ Contribution Processing │
└─────────────┬─────────────┘
│
▼
┌───────────────────────────┐
│ Recordkeeping │
└───────┬───────────┬───────┘
│ │
▼ ▼
┌──────────────┐ ┌───────────────────────────┐
│ Investment │ │ Distribution Management │
│ Management │ └───────────────────────────┘
└──────────────┘
This map gives us boundaries, but boundaries alone don’t explain how the system actually functions.
For example, when Contribution Processing accepts a $500 contribution for a participant, Recordkeeping must process it.
If we model it simply as:
Plaintext
[Contribution Processing] ────( API Call )────> [Recordkeeping]
That’s a basic integration diagram, but it leaves critical design questions unanswered:
- Who owns the definition of a “contribution”?
- What happens if Contribution Processing calls it
PayrollDeduction, while Recordkeeping calls itContribution? - What if the upstream payload structure changes without warning?
- What if Recordkeeping only needs two fields out of twenty?
DDD focuses on the nature of the relationship, not just the transport layer.
What Is a Context Map?
A context map is a high-level strategic view of bounded contexts and the specific patterns governing their interactions.
Plaintext
┌─────────────────────────┐
│ Contribution Processing │ (Upstream / Supplier)
└────────────┬────────────┘
│
│ Event: ContributionAccepted
│ Pattern: Published Language / Customer-Supplier
▼
┌─────────────────────────┐
│ Recordkeeping │ (Downstream / Customer)
└─────────────────────────┘
Upstream vs. Downstream
Relationship direction is defined by influence:
- Upstream (U): Produces data or capabilities. Changes here ripple down.
- Downstream (D): Consumes data or capabilities. Depends on the upstream context.
Note: Upstream does not mean “more important.” It simply indicates direction of dependency.
If Contribution Processing (Upstream) changes its payload schema from:
JSON
{
"participantId": "P-1001",
"amount": 500.00
}
to:
JSON
{
"workerId": "P-1001",
"deductionAmount": 500.00
}
A directly coupled downstream context breaks. The goal of context mapping is to choose how downstream systems handle these changes.
Key Context Mapping Patterns
| Pattern | Dynamic | Best Used When… |
| Customer/Supplier | Upstream plans work to support downstream needs | Teams collaborate closely and agree on contracts |
| Conformist | Downstream adopts upstream model entirely | Integrating with immutable APIs or internal standard models |
| Anti-Corruption Layer (ACL) | Downstream translates upstream data into its own terms | Protecting core domains from third-party or legacy models |
| Published Language | Standardized, well-documented event/data format | Multiple downstream consumers read the same context |
| Shared Kernel | Two contexts share a small subset of code/models | Extremely stable concepts (e.g., Money, Currency) shared by close teams |
| Partnership | Two contexts evolve together with joint releases | Highly coupled domains built by closely aligned teams |
1. Customer / Supplier
The upstream context acts as a supplier, building endpoints or events tailored to the downstream customer’s needs.
Plaintext
┌─────────────────────────────────┐
│ Contribution Processing (U) │
│ [SUPPLIER] │
└────────────────┬────────────────┘
│
│ Agreed Integration Contract
▼
┌─────────────────────────────────┐
│ Recordkeeping (D) │
│ [CUSTOMER] │
└─────────────────────────────────┘
The teams agree on a clear contract payload:
JSON
{
"eventType": "ContributionAccepted",
"contributionId": "C-10045",
"participantId": "P-2048",
"planId": "PLAN-77",
"amount": {
"value": 500.00,
"currency": "USD"
},
"contributionType": "EmployeeDeferral",
"effectiveDate": "2026-08-20"
}
2. Conformist vs. Anti-Corruption Layer (ACL)
When integrating with external systems like a third-party payroll provider, you rarely get to negotiate the API contract:
Plaintext
"Here is our API payload. Take it or leave it."
You have two choices:
Choice A: Conformist (Accept the foreign model)
You use the third party’s naming directly in your core domain.
C#
// Domain model polluted by vendor vocabulary
public class Contribution
{
public string EmployeeNumber { get; set; } // Vendor term
public string DeductionCode { get; set; } // Vendor term
public decimal DeductionAmount { get; set; } // Vendor term
}
Choice B: Anti-Corruption Layer (Translate at the boundary)
You insert an adapter between the external payload and your clean domain model.
Plaintext
┌────────────────────────┐
│ External Payroll System│ (Payload: employee_number, deduction_amount)
└───────────┬────────────┘
│
▼
┌────────────────────────┐
│ Anti-Corruption Layer │ (Translates external terms into domain concepts)
└───────────┬────────────┘
│
▼
┌────────────────────────┐
│ Contribution Processing│ (Domain: ParticipantId, Money, ContributionType)
└────────────────────────┘
C#
// Translation inside the ACL
public Contribution Translate(PayrollDeduction externalPayload)
{
return Contribution.Create(
ParticipantId.From(externalPayload.EmployeeNumber),
Money.Usd(externalPayload.DeductionAmount),
ContributionType.EmployeeDeferral,
externalPayload.PayDate
);
}
3. Context Boundaries in Action: Contribution vs. Transaction
A common modeling mistake is reusing domain objects across boundaries because they represent similar real-world events.
- In Contribution Processing, a
Contributionis a business action representing money submitted by an employer. - In Recordkeeping, a
Transactionis a financial record updating an account balance.
Instead of sharing one generic Contribution class, let each context translate the incoming boundary event into its local model:
Plaintext
Contribution Processing Context
─────────────────────────────────────────────────
[ Event: ContributionAccepted ]
───────────────────────┬─────────────────────────
│ Context Boundary (Event Bus)
───────────────────────▼─────────────────────────
Recordkeeping Context
[ Receive: ContributionAccepted ]
│
▼
[ Domain Action: Create AccountTransaction ]
│
▼
[ Update Balance: ParticipantAccount ]
Complete Context Map for Acme Retirement Services
Bringing these concepts together results in an explicit Context Map:
Plaintext
┌─────────────────────────┐
│ Plan Administration │
└────────────┬────────────┘
│ (U)
│ Pattern: OHS / Published Language
│ Contract: PlanRules & Eligibility
▼ (D)
┌─────────────────────────┐
│ Participant Management │
└────────────┬────────────┘
│ (U)
│ Contract: ParticipantUpdated
▼ (D)
┌──────────────────┐ ACL ┌─────────────────────────┐
│ External Payroll ├──────►│ Contribution Processing │
└──────────────────┘ (CF) └────────────┬────────────┘
│ (U)
│ Pattern: Published Language
│ Contract: ContributionAccepted
▼ (D)
┌─────────────────────────┐
│ Recordkeeping │
│ ─────────────────────── │
│ - Account Transactions │
│ - Balances │
└───────┬─────────┬───────┘
│ (U) │ (U)
Customer/Supplier Pattern │ │ Customer/Supplier Pattern
▼ (D) ▼ (D)
┌────────────┐ ┌─────────────────────────┐
│ Investment │ │ Distribution Management │
│ Management │ └─────────────────────────┘
└────────────┘
Practical Steps to Map Your Contexts
To map context relationships without getting bogged down:
- Identify the Data Flow: What explicit event or data payload crosses the boundary?
- Determine Model Ownership: Who defines the contract format?
- Assess Team Dynamic: Is the relationship collaborative (Customer/Supplier), rigid (Conformist), or co-dependent (Partnership)?
- Define the Defense: Does downstream need an Anti-Corruption Layer (ACL), or can it consume a Published Language directly?
- Keep Shared Kernels Minimal: Limit shared code packages to primitive Value Objects like
MoneyorCurrency. Avoid sharing core domain entities across boundary lines.
What’s Next: Entities and Value Objects
We have defined our strategic boundaries and how they communicate. Next, we step inside a single bounded context—Recordkeeping—to build its internal domain model.
In Part 5: Tactical Modeling with Entities and Value Objects, we will cover:
- Defining identity vs. equality (
ParticipantAccountvs.Money). - Designing immutable domain types to make invalid states impossible.
- Replacing primitive types (
decimal amount,string type) with rich domain concepts.
Discover more from TACETRA
Subscribe to get the latest posts sent to your email.