Domain-Driven Design for Service Boundaries

ModernLift · ·11 min read
Part 5 of 10

Domain-driven design finds service boundaries by mapping the business into bounded contexts — areas where a term has one consistent meaning and the rules are coherent — and drawing service lines along those contexts rather than along technical layers or database tables. Good boundaries keep what changes together inside one service and minimize the calls between services. The test of a boundary is coupling: if two services constantly have to change together, the boundary is in the wrong place.

Part 4 kept giving the same instruction — cut along the domain boundaries — and kept deferring the question of how to find them. This is that part. It is the most intellectually demanding step in the whole migration, and the one where a quiet mistake costs the most, because a boundary in the wrong place is invisible until the services are built and refusing to come apart.

The discipline that addresses this is domain-driven design (DDD), introduced by Eric Evans in his 2003 book of the same name. You do not need to adopt all of DDD to decompose a monolith well. You need one idea from it, used seriously: the bounded context.

Why technical boundaries fail

The intuitive way to split a system is along its technical structure — a service for the data layer, a service for the API, a service per database table, a service per existing module. Every one of these produces bad boundaries, for the same reason: they cut across the business instead of along it.

Split by table and a single business operation — place an order — has to touch the orders service, the inventory service, the pricing service, and the customer service in one breath, because the data it needs was scattered by the table layout. Now every order is four network calls, four failure points, and four services that must be deployed together to ship one change. You have paid the full price of distribution and bought none of the autonomy. That is the distributed monolith, and a bad boundary is its most common cause.

The reversal worth keeping: a boundary is not where the code is divided — it is where the business is.

The bounded context

A bounded context is a region of the business within which a term has one precise meaning and the rules form one coherent model. The classic example is the word customer. In billing, a customer is an account with a payment method and an invoice history. In support, a customer is a person with tickets and a satisfaction score. In shipping, a customer is an address and a delivery preference. Same word, three genuinely different concepts.

A monolith usually smears these into one bloated “Customer” object that tries to be all of them at once, accumulating fields and rules from every part of the business until no one can change it safely. DDD says: stop. Each of those is a different model living in a different bounded context. Make the boundary explicit, let each context own its own version of “customer,” and you have found three natural service boundaries.

A bounded context maps cleanly to a service because, by construction, it is internally coherent and externally decoupled — exactly the property a good service needs.

How to find the contexts in your system

You find bounded contexts by listening to the business, not by reading the schema. A few concrete techniques:

  • Listen for language that shifts. When the same word means different things in two parts of the business — or different words mean the same thing — you have found a context boundary. The language changes because the model changes.
  • Map business capabilities, not data. Ask what the business does — capture an order, authorize a payment, reserve inventory, resolve a ticket — and group the capabilities that belong to one coherent area. Those groups are candidate contexts.
  • Run an event-storming session. Get the people who actually understand the domain in a room and walk the business events in order — order placed, payment authorized, inventory reserved, shipment dispatched. The clusters of events, and the handoffs between clusters, reveal the contexts and their boundaries.
  • Follow the change. Look at history: which parts of the code consistently change together, and which change independently? Things that change together belong in one context; things that change apart belong in different ones.

This is exactly the work our discovery process does first — reading the codebase, data, and domain rules and capturing them as living specs — because you cannot draw a boundary through a domain you have not mapped.

The test of a good boundary: coupling

You will rarely get every boundary right on paper, so you need a test you can apply. The test is coupling, measured two ways:

  1. Do the two services have to change together? If a change to one almost always forces a change to the other, they are not really two contexts — they are one, split in the wrong place. Merge them.
  2. How chatty is the seam? If completing one normal business operation requires many synchronous calls back and forth across a boundary, the boundary is cutting through a tightly-coupled process. The right boundary is where the conversation across it is occasional, not constant.

A good boundary keeps what changes together, together, and makes the traffic across it sparse. A bad one does the opposite, and no amount of clever infrastructure rescues it.

Coarse first, fine later

A practical bias that saves a lot of pain: prefer fewer, larger services to start. It is far easier to split a service that turned out to be too big than to merge two services that should never have been separate — merging means re-entangling data and code you already pulled apart, the most expensive move in the migration.

So when in doubt, draw the boundary coarse. Extract a whole context as one service, run it, and let real usage show you whether it wants to divide further. The mistake is almost always over-splitting too early, chasing the smallest possible services because “micro” is in the name. Service size should follow the domain, not an adjective.

Where DDD hits its limits

DDD is a method, not an oracle, and it has real limits. Bounded contexts are a model of the business, and the business sometimes resists clean division — there are genuinely entangled domains where any boundary you draw will be imperfect and some coupling is unavoidable. In those cases the honest move is to acknowledge the coupling, keep the entangled parts together in one larger service rather than pretend a clean line exists, and revisit later if the domain itself clarifies. Forcing a boundary through a genuinely indivisible process does not produce two clean services; it produces two broken ones. And no model survives first contact unchanged — expect to adjust boundaries as the extraction teaches you what the diagram could not.

Where this leads

You can draw a perfect map of bounded contexts and still hit the wall that stops most decompositions cold: the data. The code may divide cleanly along your boundaries, but the database underneath it is one shared schema where everything joins to everything. Part 6, Decomposing the Database, takes on the hardest seam in the whole migration — splitting shared data along the boundaries you just found, without losing the consistency the monolith gave you for free.

Frequently asked questions

How do you decide microservice boundaries?
Draw them along bounded contexts — the natural divisions of the business where a given term has a single, consistent meaning and the rules form a coherent whole. Things that change together should live in the same service; things that change independently belong in different ones. Boundaries drawn this way minimize cross-service calls and keep each service genuinely autonomous. Boundaries drawn along technical layers or shared tables produce services that must change in lockstep.
What is a bounded context?
A bounded context, from domain-driven design, is a boundary within which a domain term has one precise meaning and one consistent model. "Customer" in billing (an account with a payment method) and "customer" in support (a person with a ticket history) are different concepts that happen to share a word. A bounded context makes that explicit and contains each model, which makes it an excellent candidate for a service boundary.
Why are wrong service boundaries so costly?
Because a boundary in the wrong place couples the services across it: they share data, change together, and chatter constantly over the network. You then have all the cost of distribution — latency, distributed data, operational overhead — with none of the autonomy that was supposed to justify it. That is the distributed monolith. Re-drawing a boundary after services have shipped is far more expensive than getting it right before extraction.
All 10 parts of Monolith to Microservices →