- DDD
- Microservices
- Software Architecture
Splitting into bounded contexts is not splitting into microservices
Finding the context boundaries is mandatory in a system that grows. Turning each one into a microservice is optional, and almost always too early. How to split the model without inheriting the cost of a fleet of services you never needed.

A Bounded Context delimits the applicability of a particular model, so that team members have a clear and shared understanding of what has to be consistent and how it relates to other contexts.
Eric Evans, Domain-Driven Design
Every post in this series moved the same boundary around inside one codebase: data versus behavior, entity versus service, application versus domain. This one zooms all the way out, to the boundary that organizes an entire system: the bounded context, the region where one model holds and each word means one thing.
Finding those boundaries is the real work, and in a system that grows it's mandatory. The mistake I want to go after here isn't technical, it's reflexive: the moment people identify a context boundary, the automatic next step becomes "so that's a microservice". And splitting the model is not the same thing as splitting the deployment. One is a domain decision the system forces on you. The other is an operational decision that, most of the time, you didn't need to make yet.
The examples are language-agnostic here, because this is about architecture, not syntax.
Two different boundaries
A bounded context is a boundary of meaning. It's the region where a particular model applies and every term has one definition. Inside the Billing context, Customer means a payer: credit limit, tax id, invoices. Inside the Shipping context, Customer means a recipient: address, delivery instructions, access notes. Same word, two models, and neither is wrong. The line between them is a bounded context boundary. Evans' whole point is that a model is only coherent within such a boundary, and trying to make one Customer serve both contexts produces a model that serves neither.
A microservice is a boundary of deployment. It's a unit that runs in its own process, owns its own database, and ships on its own release cycle. It's a runtime and operational concern: independent scaling, independent deploys, a network hop at the edge.
One is about what words mean. The other is about what runs where. They can coincide, but they are not the same line, and the more important of the two is the one about meaning. You split the model because the domain asks for it. You split the deployment only when operational reality asks for it, and it asks far less often than people assume.
The part that matters is splitting by meaning
The work that pays off is finding the linguistic seams: where a term changes meaning, where a model stops being coherent. That's domain design, and you do it regardless of how the system will run.
And here's the part that usually gets skipped: you do all of that work inside a single deployable. A well-bounded context is a module, with its own model, a clean interface, and no hidden dependencies on the other modules. Several of those together make a modular monolith, which has almost every benefit people attribute to microservices (clear boundaries, teams owning areas, low coupling) with none of the network cost.
A well-built modular monolith is the right destination for most systems. The boundaries are there, they're real, and the day one of them needs to become a service, the cut is already drawn. Splitting by meaning first is what makes that promotion cheap later.
The reflex of turning everything into a microservice
The trouble starts when "found a context" becomes, immediately, "so I stand up a service". Multiply that reflex by the eight or ten contexts of a real system and you've just traded a code problem for an operations-and-coordination problem nobody asked for.
The cost shows up on two fronts.
Operational complexity you adopted for free. In the same process, a call between contexts is a method call: synchronous, transactional, with a stack trace. Between services it becomes a network hop, with latency, partial failure, timeouts, retries, eventual consistency, and distributed tracing. None of that solves a problem of yours. It's just the entry fee for distribution, and you pay it on every request, forever.
Management complexity nobody counts. Each service is a repository, a CI/CD pipeline, a dashboard, an on-call rotation, a deploy to coordinate, and in practice an owner. A context that was a folder in your project becomes an operational unit that needs people minding it. Ten contexts become ten of those. The cost isn't the code, it's the coordination surface between systems and between teams that now exists. You don't scale engineering, you scale meetings.
This is overengineering in the literal sense: you pulled forward the cost of a scale that hasn't arrived. Microservices solve problems of independent scaling, team autonomy, and fault isolation. If you don't have those problems, you bought the bill without buying the benefit.
The aggregate is the line you never cut
There's one place where splitting the deployment isn't just expensive, it's wrong. The classic case: "we do microservices" turns into a service per entity, an order-service, an item-service, a status-service, each with its own database.
But Order, OrderItem, and OrderStatus are one aggregate. The aggregate is the consistency boundary, the thing that has to change as a unit, in one transaction. Split it across services and that transaction becomes a distributed one. order.submit(), which used to be a method call that either fully happened or didn't, is now a saga with compensating actions, and "an order and its items are always in sync" turns from an invariant you enforced in memory into a distributed-systems problem you monitor in production.
You never split a single aggregate across services. That's the one hard rule here, and it comes before any conversation about topology.
When the boundary is false, only the cost is left
The subtler damage is drawing the service boundary somewhere that isn't a real context boundary at all. Split by table, by layer, or by "each team owns a service" without regard to the model, and you get pieces that are still deeply coupled, they just talk over HTTP now.
Every change still touches three services. Every deploy still has to be coordinated. You've taken on the full operational cost of distribution and gotten none of the benefit, because the boundary you drew doesn't correspond to anything the domain actually separates. That's a distributed monolith: the coupling of a monolith with the operational pain of microservices, the worst of both worlds.
How the two actually map
The relationship isn't one-to-one. A single bounded context can be:
- A module in a monolith. The default case, and the right one for most systems. A well-bounded context with a clean interface is a great module. No network required.
- Exactly one microservice. When there's a genuine need: the Ordering context becomes the
ordering-service. - Several microservices. A large context might split for scaling or team reasons, as long as the split doesn't cut an aggregate.
What never happens: a single aggregate spread across services, and a single service straddling two contexts with two conflicting models of the same word.
So the design order matters, and so does the leaning. First find the context boundaries, from the language. Then, and only then, decide how to deploy them, and start by assuming the answer is "module". The context boundary is a domain decision, mandatory. The service boundary is a deployment decision made downstream, and its default is "not yet".
When a context really should become a service
None of this is an argument against microservices. It's an argument against microservices by reflex.
A bounded context is exactly the seam you want to promote to a service when you have a reason: it's loosely coupled by design, it talks through well-defined contracts, and it tends to align with team ownership. When a concrete need appears (that context has to scale on its own, or have its own release cadence, or stay isolated from a failure that takes down the rest, or a team needs full autonomy over it), promoting that context to its own service is the right move. And because you already split by meaning, the cut is already there.
The practical rule: don't ask "could this context be a service?", because the answer is almost always yes and it doesn't help you. Ask "what concrete problem does making it a service solve that a module doesn't?". If you can't name the problem, the answer is module.
How to spot it in your codebase
- Many small services, one context each, with no scaling or isolation need behind them. The most common sign of overengineering: distribution adopted by default, not by problem.
- A distributed transaction (or saga) holding one aggregate together. The clearest sign an aggregate got split across services.
- Services that can't deploy independently. If a change to one always forces a coordinated release of others, the boundary isn't real and you're paying for a network hop on top of coupling that's still there.
- Services named after tables or layers, not after areas of the business.
entity-serviceandvalidation-serviceare technical splits, not context splits.
What's next
This closes the arc of the series: from behavior leaking out of an object, all the way up to boundaries drawn across a whole system. The common thread was always the same question, asked at different scales: is this boundary drawn from the domain, or from something incidental (a framework, a layer, a deployment diagram)?
This whole post argued that the right destination for most contexts is a module, not a service. What's missing is the practical part: how to keep those boundaries from blurring inside the same deployable, given that "we agreed not to touch each other's internals" doesn't survive a tight deadline. The next post in the series shows it with code, using Spring Modulith in Java: how to structure a real modular monolith, make the contexts explicit in the package structure, and have a test that breaks the build when one module reaches into what it shouldn't.
After that, the post that closes the series steps back from all of it to ask the question underneath every one of these: when should you not reach for DDD at all?
Ever seen a system with ten microservices that could comfortably have been ten modules of one monolith? Reach out on LinkedIn, I'd love to hear about it!