# Context Mapping in DDD: Relationships Between Bounded Contexts

> Conformist, partnership, customer-supplier: context mapping patterns are not diagram decorations. They are power dynamics that predict which team absorbs breaking changes. Here is each pattern, when it happens to you, and what it looks like in .NET code.

Published: 2026-09-22. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/context-mapping-ddd

**Context mapping** documents how bounded contexts and their teams work together.
It identifies who shapes shared contracts, which models require translation, and how changes reach downstream consumers.
Use it to choose relationships such as partnership, customer-supplier, or an anti-corruption layer before those dependencies become expensive to change.

## What Do Upstream and Downstream Mean?

A [**bounded context**](https://milanjovanovic.tech/blog/bounded-context-ddd-explained) is a boundary inside which a model and its language stay consistent.
Context mapping is the [**strategic DDD**](https://milanjovanovic.tech/blog/strategic-vs-tactical-ddd) practice of documenting how those boundaries relate.

In an upstream-downstream relationship, influence has a direction:

- The **upstream** context's decisions flow down: its model, its schema, its release schedule.
- The **downstream** context receives those decisions and must cope.

Upstream and downstream describe **influence**, not data flow.
A context that consumes your events can still be upstream of you politically, if it's the ERP system that the business will never change for your convenience.

![The upstream context](https://milanjovanovic.tech/blogs/articles/context-mapping-ddd/upstream-downstream.png)

Other relationships, such as partnership, do not have a single upstream owner.
The [**DDD Reference**](https://www.domainlanguage.com/ddd/reference/) describes these patterns as choices about model ownership and team coordination.

## Partnership: Coordinate Changes

Two contexts succeed or fail as a unit: Ordering and Fulfillment in the same product, owned by two teams that plan together.
Neither is upstream.
Interfaces change by mutual agreement, in the same planning cycle.

A partnership means the teams plan integration work together and commit to each other's success.
They can still use compatible contracts and deploy separately.
The cost is ongoing coordination; a partnership becomes difficult when one team's priorities consistently override the other's.

## Shared Kernel: Share a Small Model

Two contexts share a piece of the model itself: a library of common types, a shared schema segment.
Every change to the kernel needs sign-off from both sides, which means **both teams absorb every change, by construction**.

In .NET this is the `SharedKernel` project with `Money`, `EntityId` types, and base classes that both modules reference.
Sharing those types couples both contexts to their semantics and changes.
Keep it small and explicitly co-owned.
A shared kernel can contain business rules, but only rules that both contexts agree to share; common utility classes alone do not make it a shared domain model.
I've written about where the line sits in [**the shared kernel pattern in a modular monolith**](https://milanjovanovic.tech/blog/shared-kernel-pattern-modular-monolith).

The failure mode: the kernel becomes a dumping ground, and soon two "independent" contexts can't release separately.
If the kernel grows faster than it stabilizes, split it and downgrade the relationship.

## Customer-Supplier: Negotiate Downstream Needs

Downstream is a **customer**: its needs enter the supplier's backlog, contract changes are negotiated, deprecation periods exist.

The mechanics in a .NET system usually look like:

- The supplier publishes versioned contracts (a NuGet package of message types, an OpenAPI spec).
- Breaking changes ship as a new version alongside the old one, with a migration window.
- Consumer-driven contract tests in the supplier's CI make downstream's expectations executable.

Contract tests make expectations visible, but the relationship also needs an agreement about prioritizing downstream work.
The supplier and customer both incur change costs.
The techniques from **message contract versioning** are how the supplier keeps its side of the bargain.

## Conformist: Adopt the Upstream Model

Upstream publishes a model; downstream adopts it as-is.
The downstream team chooses to use the upstream model rather than maintaining its own translation.
Your Orders module stores the payment provider's `TransactionRecord` shape in its own tables, names and all.

Conformist gets a bad reputation it doesn't fully deserve.
It can reduce initial mapping work when the upstream model already fits the downstream use case.
For a peripheral concern, that trade can be right.

The tradeoff is **dependence on the upstream model**.
Upstream renames a field, and the rename ripples through your entities, your database, your business logic.
You've outsourced part of your model's evolution to a team that doesn't know you exist.

Conforming can be reasonable for a peripheral integration.
For a core model, consider how much upstream terminology you are willing to adopt.
The question to ask in review: "if this upstream model changes shape next quarter, how many of our files change?"
If the answer is "most of the module", you've conformed on something core.

## Anti-Corruption Layer: Translate Into the Local Model

The ACL is the downstream position for models you refuse to import.
A translation layer sits at the boundary, converting upstream's language into yours:

The adapter can translate status codes without exposing the external response to application code.
This example uses a fictional billing API; its three status codes are part of that example's contract:

```csharp
public enum PaymentStatus { Approved, Declined, Unavailable }
public sealed record PaymentResult(PaymentStatus Status, string? Reference);

public interface IPaymentGateway
{
    Task<PaymentResult> GetStatusAsync(string paymentId, CancellationToken ct);
}

// Infrastructure-only contract from the fictional upstream system.
public sealed record LegacyPayment(string StatusCode, string Reference);

public interface ILegacyBillingClient
{
    Task<LegacyPayment> GetPaymentAsync(string paymentId, CancellationToken ct);
}

public sealed class LegacyBillingAdapter(ILegacyBillingClient client)
    : IPaymentGateway
{
    public async Task<PaymentResult> GetStatusAsync(
        string paymentId, CancellationToken ct)
    {
        var payment = await client.GetPaymentAsync(paymentId, ct);

        return payment.StatusCode switch
        {
            "00" => new(PaymentStatus.Approved, payment.Reference),
            "51" => new(PaymentStatus.Declined, null),
            "91" => new(PaymentStatus.Unavailable, null),
            _ => throw new InvalidOperationException(
                $"Unsupported billing status: {payment.StatusCode}")
        };
    }
}
```

Application code sees `IPaymentGateway`, `PaymentResult`, and `PaymentStatus`.
`LegacyPayment` stays inside Infrastructure.
A new code requires an explicit mapping decision; it must not silently become a successful payment.

The adapter and its contract tests need maintenance when upstream changes.
Translation contains many changes, but a new business meaning can still require changes in the local model.
I go deeper on the mechanics in [**the anti-corruption layer pattern**](https://milanjovanovic.tech/blog/anti-corruption-layer-ddd).

Choose an ACL when the external model differs from the local model enough to justify maintaining a translation.

## Open Host Service and Published Language

Flip to the upstream perspective.
If five downstream contexts each negotiate custom integrations with you, you're running five customer-supplier relationships.
The **open host service** consolidates them: one well-designed public API, one **published language** (your OpenAPI spec, your event schema), offered to all comers on equal terms.

This is what a well-run platform team does, and it's also what a module's public API is in a [**modular monolith**](https://milanjovanovic.tech/blog/internal-vs-public-apis-in-modular-monoliths): a deliberate contract, distinct from the module's internals, that all other modules consume.

The upstream cost: the published language must evolve like a product, with versioning and deprecation discipline.
A common contract reduces per-consumer customization, while support and compatibility work continue.

## Separate Ways: Avoid an Unnecessary Integration

If integrating two contexts costs more than duplicating a small feature, **don't integrate**.
Two modules each keeping their own tiny copy of "country codes" is not a DRY violation; it's two teams declining to couple their release schedules over a lookup table.

## Reading Your Own Map

The exercise that makes this practical: list every pair of communicating contexts (or modules; the [**communication patterns in a modular monolith**](https://milanjovanovic.tech/blog/modular-monolith-communication-patterns) map one-to-one), and for each pair write down who absorbs a breaking change today.
Record current behavior before proposing changes.

Then compare against intent:

- Core domain conforming to an external system: promote to ACL.
- A partnership dominated by one team: clarify whether downstream needs will be negotiated or simply accepted.
- Shared kernel that changes weekly: shrink it or split it.
- An integration nobody can justify: separate ways.

The map won't fix the power dynamics, but it makes them visible, and visible dynamics can be renegotiated.
These decisions connect team ownership to the [**strategic DDD**](https://milanjovanovic.tech/blog/strategic-vs-tactical-ddd) model.

## Summary

- **Partnership** means coordinating integration decisions and priorities.
- **Shared kernel** means co-owning a small portion of the domain model.
- **Customer-supplier** gives downstream needs a negotiated place in upstream planning.
- **Conformist** adopts upstream semantics; an **anti-corruption layer** translates them.
- **Open host service** and **published language** provide common integration contracts.
- **Separate ways** avoids integration when its cost exceeds its benefit, while accepting any duplication.

Keep the map current when ownership, contracts, or release agreements change.
It should explain today's dependencies well enough to guide the next change.

Thanks for reading.

And stay awesome!

---

## Frequently asked questions

### What is context mapping in DDD?

Context mapping is the strategic DDD practice of documenting how bounded contexts relate: which one is upstream, which is downstream, and what translation happens between them. The map captures team and power relationships, not just data flow.

### What does upstream and downstream mean in a context map?

The upstream context influences the downstream one: its model and release schedule flow down. When upstream changes its contract, downstream absorbs the impact. Upstream and downstream describe influence, not the direction data moves.

### What is the difference between conformist and anti-corruption layer?

Both are downstream positions. A conformist adopts the upstream model as-is, saving translation effort but importing every upstream concept and change. An anti-corruption layer translates the upstream model into the local one, costing ongoing effort but protecting the local model.

### When should two teams use a shared kernel?

Rarely, and only when both teams can coordinate every change to the shared model, such as two closely collaborating teams in one codebase. A shared kernel couples release schedules, so most pairs of contexts are better off with published contracts.

### Do context mapping patterns apply inside a modular monolith?

Yes. Modules in a modular monolith are bounded contexts, and every pair of communicating modules has a relationship: one conforms to the other, translates its events, or shares a kernel of common types. Naming the pattern makes the coupling visible and intentional.
