Thomas Coopman
CEO and Senior Consultant
Thomas Coopman explores how to build bounded context maps
When connecting bounded contexts, the line between a clean integration and a tangled mess is thin. This session skips the basic Domain-Driven Design theory and dives straight into the practical realities of context mapping.
With two bounded contexts, we represent one as upstream and the other as downstream. Choosing which bounded context is upstream versus downstream is an important decision. This presentation explores that choice and how applying a Conformist or Anti-Corruption Layer impacts your architecture.
Through practical scenarios and code examples, we’ll explore what these patterns actually mean for your codebase, your API contracts, and your team's autonomy.
We’ll also look at the nuances of these patterns in practice—showing that these boundaries aren't strictly black and white, and why you might intentionally design a "partial" ACL.
What you will learn:
What being upstream or downstream actually means for your integrations.
The practical, day-to-day code implications of choosing a Conformist vs. Anti-Corruption Layer.
Code examples demonstrating how to map and protect these boundaries.
Why these patterns aren't binary—and how you can implement a hybrid or partial ACL when it makes sense.
Thomas Coopman is CEO and a senior consultant at Aardling. He works with clients across finance, supply chain, and automotive.
Watch the full video below 👇
A question I keep running into during consulting and training is what to do with bounded context relationships. Teams get stuck on upstream and downstream — which one is which — and then on whether to build an anti-corruption layer or just be conformist. Some teams don't seem to care about it at all. Others are stuck on what to do. Neither is a great place to be. So I wanted to spend this presentation on a narrow and fairly deep topic, and try to make these decisions more concrete and more useful inside a team.
Three definitions first, so we're working from the same starting point. The ubiquitous language is a designed, explicit language that lives on the solution side — not simply the domain language, but something made much more specific for the solution. The model is the mental model of the solution, expressed most importantly in code, but also in text, in diagrams, and in people's heads. And a bounded context is, in the end, quite simple: inside a context we have designed, there is exactly one model and one ubiquitous language, they are consistent, and there is an explicit boundary around them.
That boundary is the part worth digging into. There's a boundary, but we still communicate across it — and in that communication, parts of our model and language leak into other bounded contexts. With a single bounded context, keeping the model and the language consistent is trivial. As soon as you introduce a second one, it gets harder. That's where the relationships come in.
I'm using bike ride sharing as the example domain throughout, because it's small enough to fit in a presentation and because the relationships in it aren't obvious. You only need to know two things about it. You pay a rate, defined as a price per minute, and that rate is not the same for every ride — it doesn't matter how or why it changes, only that for every ride we have to know what the current rate is. And you pay from the moment a bike is uniquely yours, so nobody else can take it, until you make it available to the platform again. That's not necessarily the same as the time you spend riding. You might reserve it up front and pay for ten minutes you're not riding. Two bounded contexts, ride and payment, and a relationship between them that we're going to work out.
The biggest misconception is that upstream and downstream describe the direction data flows. They don't. Take two services with a simple getItems API. It's already hard to say which way the data flows: one initiates the call, the other replies, so data flows both ways. It's also not about who initiates, because I can draw the same two services calling each other in the other direction. Neither of those is what upstream and downstream are about.
What all bounded context relationships describe is how models, and changes to those models, impact other bounded contexts. Upstream and downstream is the simple version of that: every time something changes in the upstream model and another model is impacted, that other model is downstream.
The most useful way I've found to phrase it is as a question about knowledge.
Does this bounded context need domain knowledge owned by that one? If yes, it's downstream.
I prefer that framing because it strips out all the technical reasons you might think you need to know about the other context, and leaves you with the only thing that matters.
One note on terminology, which I'll come back to at the end. Eric Evans' original book introducing DDD describes upstream and downstream as a team relationship — team A is upstream from team B. I'm deliberately blurring that distinction and talking about bounded contexts being upstream and downstream, because I think it makes the case stronger.
Let's have a look at the first two designs below. On the left, the design shows that every time a ride ends, ride emits a ride-ended event, and payment listens to it. For that to work, payment needs to know how a ride works — the rate, the duration. So payment is downstream and ride is upstream.
Now the design on the right says, instead of emitting an event, ride sends a command to payment. Now ride needs some knowledge of how a payment works in order to execute it, so payment is upstream and ride is downstream. Same two contexts, opposite relationship, and nothing about the domain changed. The direction came entirely out of a design decision we made.
Two heuristics stand in for the knowledge question when you're staring at a diagram. The first is language: ride-ended is clearly ride's language, and messages that go out are almost always in the upstream language. The second is that an upstream bounded context can act as if the downstream one does not exist — and the word "can" is doing the work there. Emit ride-ended and ride genuinely doesn't care how payment works. In the command version, if ride didn't exist there would be no payment, but that's not payment's problem; payment does its job whenever it gets a request. That heuristic is weaker than the other two, especially once you layer other relationship patterns on top, but it's often a good sanity check.
When a model is conformist, we're saying that we know the other model is there, we know we need it, and we are not going to protect ourselves from it in any way. We take that knowledge and put it into our own domain layer. The consequence is that our bounded context gets diluted with knowledge from another one. The way we protect ourselves is an anti-corruption layer at the boundary, which does the translation, so that only the layer needs to know about the other model.
Two things matter here. The first is that this choice belongs to the downstream bounded context. The downstream one is the one affected by the upstream one, so it's the one that decides whether to conform or to build a layer. The second is that the anti-corruption layer doesn't make knowledge disappear. The knowledge still has to live somewhere. What you're choosing is where it lives — more in the model, or more in the layer. In a lot of cases you'll want it in the layer, because that keeps your own model purer, and a pure payment model is one that other bounded contexts can also use.
That choice also determines how expensive your mistakes are. If you built an anti-corruption layer and later want to reverse the relationship, it's usually cheaper, because it often means moving the layer to the other bounded context and flipping the direction. If you were conformist, it can be really expensive, because the other model has been fully integrated into your own and pulling it apart again is hard. Conformist definitely has its place, but it makes some choices much harder to undo, which is why this question is worth asking early rather than late.
See the options we've described below — ride upstream or ride downstream, crossed with conformist or anti-corruption layer — and you have four designs. Looking at that picture alone, you cannot decide which is better, and that's the point. The decision depends on a question you have to answer first: who owns the domain knowledge, or who should own it?
For the bike domain that looks like this. How long does a ride last, and is the duration of a ride the same as the duration you pay for? If we charge per minute and a new minute starts, do we round up, round down, or calculate to the second? How do we calculate the amount to pay? Who owns the sequence of accepting a rate, starting a ride, ending a ride? And how do we link a driver ID to the account ID that actually has to be billed — we've been waving at that link the whole time without saying who owns it. The first two questions already belong together because of the rounding, and that's a signal. In this example the sequence looks obvious, but in more complex domains it stops being obvious very quickly.
Once you've answered that, a second set of questions comes in, and these are more like open heuristics than rules. Is one of them a core domain? Ride probably is, and in general we'd like core domains not to have too many upstream contexts, because every upstream change impacts them. Is there legacy involved? Making a legacy context downstream is often a bad idea, because absorbing incoming changes in legacy code is rarely trivial. How stable is it? Payment is probably quite stable, so a core domain being downstream from it may be perfectly fine. And do we own it? If our team or our company owns it we can be looser, because we're the ones making the changes. Replace payment with Stripe in any of these diagrams and everyone immediately agrees Stripe is upstream — we can't change it, and it is certainly not going to listen to our ride-ended events. Sometimes the choice isn't ours to make at all.
Apply all of that to the bike domain and you can end up somewhere none of the four options covered. Ride is core, so we want it upstream. But we also want other bounded contexts to consume payment, so we don't want payment carrying a customised, ride-specific interface. Payment is generic — a thin layer over our payment providers — and it should need the least knowledge possible.
So we introduce a third bounded context in between: a price calculator. Not a thin translation layer, but something with real responsibilities. It owns how much has to be paid. It knows what the time means exactly. The rounding rules live there. It knows how to find the correct account ID from the driver ID. We now have a specific place to put that domain knowledge, and running back through the questions confirms it belongs there. That's the honest answer to which design is better: often it's a design that wasn't on your list when you started.
It's also fair to ask whether that price calculator is just an anti-corruption layer with a nicer name. It is a kind of anti-corruption layer, but a grown-up one, because it contains knowledge of its own. And that's exactly why I wouldn't wrap it in another anti-corruption layer. We need that knowledge — it's the whole reason the context exists. Which answers the more general question of whether you should add an anti-corruption layer by default: no, you have to think about when it makes sense and when it doesn't.
A few questions come up nearly every time. Can an upstream context implement an anti-corruption layer? In principle no, because if it needs one it isn't upstream any more. Upstream means I need no knowledge of the other context, and an anti-corruption layer is precisely about translating that knowledge. Can the layer become its own bounded context? Yes, and that's essentially the price calculator. If it's only an anti-corruption layer it's probably not worth it, but if it takes on real responsibilities, or several bounded contexts can share it, then it is.
Can a bounded context be both upstream and downstream to the same one? My answer is no, and this was the one we argued about most internally. Going back to the original definition, this is a team relationship, and "both directions" feels strange between two teams — we already have a pattern for that, called a partnership, where two models work together towards a common goal. There's a second reason too: a bounded context has one consistent model and one ubiquitous language, so saying parts of your model depend on B while B depends on other parts of your model is odd, and probably a design problem. I'm not saying it never happens. I'm saying look hard at it.
Do architecture decisions constrain any of this? Yes. If you want ride to be upstream, it's awkward to design that over HTTP APIs when you have no messaging and no fire-and-forget. If you only communicate via APIs, some choices are already closed off. And does any of this hold inside a monolith? Completely — two bounded contexts in one deployment unit with an upstream and a downstream relationship is fine, and thinking about those relationships inside a monolith is a good idea. A bounded context can also span many modules in the same service, as long as you're not treating "module" as something with a fixed meaning. What you actually have to ask is whether this is one model with one ubiquitous language and a clear purpose. Beyond that, there are no rules about what a bounded context should look like.
Upstream and downstream are not about data flow, and not about who initiates the call. They're about which model is impacted when another model changes. The practical test is a knowledge question — does this bounded context need domain knowledge owned by that one — and two heuristics will get you most of the rest of the way: whose language the messages use, and whether the upstream could act as if the downstream didn't exist.
Conformist or anti-corruption layer is the downstream context's decision, and it's a decision about where knowledge lives, not whether it exists. Conformist is cheap now and expensive to reverse.
You can't pick a design off a diagram. You pick it by answering who should own which domain knowledge, then layering on questions about core versus generic, legacy, stability and ownership. And if you're wondering whether this matters with only one or two teams, it does — not for the communication overhead, which is the usual argument, but because it changes how you design. If we don't think about it at all, the chances we arrive at something like a separate price calculator are very low. We find that design precisely because we start adding constraints to the model, notice that some things don't feel right, and try to fix them.
CEO and Senior Consultant