Context Mapping in DDD: Relationships Between Bounded Contexts

Context Mapping in DDD: Relationships Between Bounded Contexts

By

7 min read··

architecturedddmodular-monolith

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 is a boundary inside which a model and its language stay consistent. Context mapping is the strategic 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's model, schema, and release schedule flow down to the downstream context, which absorbs the cost when an upstream breaking change lands

Other relationships, such as partnership, do not have a single upstream owner. The 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.

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:

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.

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: 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 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 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.

  • Bounded Context in DDD Explained With Examples

    Bounded contexts let Ordering and Fulfillment use different models of the same concepts. Define those boundaries in .NET, translate integration contracts, and keep ownership of models and data explicit.

  • EventStorming for .NET Teams: A Practical Guide

    EventStorming brings domain experts and developers together to explore a business process through events. Use the timeline to expose missing rules, investigate possible bounded contexts, and choose which behaviors to model in .NET.

  • Aggregate Design in DDD - Rules, Boundaries, and Consistency

    Aggregates are the most important tactical pattern in Domain-Driven Design. They define consistency boundaries, enforce invariants, and protect your domain model. Here are the rules and practical guidance for designing aggregates in .NET.

Loading comments...

Whenever you're ready, there are 4 ways I can help you:

  1. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  2. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  3. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  4. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

Become a Better .NET Software Engineer

Join 66,000+ engineers who are improving their skills every Saturday morning.