Poor microservices architectures usually fail at the boundaries, not the technology.
A common mistake is splitting services around database tables instead of business capabilities. This creates services that constantly depend on each other to complete simple workflows.
Domain-Driven Design helps avoid this by shaping software around the business domain. When service boundaries follow the domain, services are usually more cohesive and less tightly coupled.
This chapter covers the DDD ideas that help define service boundaries and when DDD may be unnecessary.
DDD has two halves, and conflating them is a common way to misunderstand it. The strategic half is about the big picture: how you divide a large domain into smaller, self-contained pieces and how those pieces relate. The tactical half is about the small picture: how you model the objects and rules inside one of those pieces.
For finding service boundaries, the strategic half is what matters. Subdomains, bounded contexts, and context maps are the patterns that tell you where one service should end and the next should begin. The tactical patterns, entities, value objects, and aggregates, govern the internal design of a single service once you have decided it exists.
The practical takeaway is an order of operations. You apply strategic DDD first to carve the domain into bounded contexts, each of which becomes a candidate service.
Only then do you use tactical DDD to design what lives inside each one. Reversing that order, designing aggregates before you know the boundaries, is how you end up modeling the whole company as one giant tangled object graph.
One tactical pattern, the aggregate, does leak into boundary decisions because it defines where transactions can and cannot reach, so we treat it carefully later in the chapter. Everything else tactical stays inside the service.
Before you can model a domain, you have to break it into its natural parts. DDD calls these subdomains, and it sorts them into three kinds based not on how complex they are but on how much they matter to the business.
A core subdomain is where the company competes. It is the thing the business does that its rivals do not, the reason customers choose it. For a ride-hailing company, the matching and pricing engine is core. This is where your best engineers belong and where custom, carefully modeled software pays off.
A supporting subdomain is necessary for the business to function but is not a differentiator. It has to exist, and it often has company-specific rules, but no customer picks you because of it. Driver onboarding or a ratings system might fall here. It deserves solid engineering, without your sharpest minds.
A generic subdomain is a solved problem that every business needs and nobody competes on, such as authentication, payments, notifications, or mapping. The usual choice here is to buy rather than build, integrating Stripe, Auth0, or Twilio instead of writing your own.
| Subdomain type | What it is | Build or buy | Where to invest |
|---|---|---|---|
| Core | The reason the business wins; its competitive edge | Build, with your best people | Heavy custom modeling |
| Supporting | Necessary, company-specific, but not a differentiator | Build simply, or outsource | Moderate; keep it clean |
| Generic | A commodity every business needs | Buy or adopt off-the-shelf | Minimal; integrate and move on |
This classification has direct consequences for architecture. Effort and autonomy should flow to the core. You would not split your most important competitive capability across five thin services, nor would you build your own payment ledger when a generic solution exists.
Naming which subdomains are core versus generic reflects reasoning about the business rather than only naming components, and it tells you where to spend limited design time.
The same word often means different things to different parts of a business, and that ambiguity causes bugs.
Ask the sales team what a "customer" is and they will describe a lead with a pipeline stage. Ask support and they will describe an account with a ticket history. Ask billing and they will describe a payment method attached to an invoice. Same word, three different concepts.
DDD's answer is the ubiquitous language: within a given part of the domain, the team, the code, and the business all agree to use the same terms for the same things, and that agreement is binding.
The class is named what the domain expert calls it. The database column matches. The conversation in the standup uses the same word as the API endpoint.
There is no translation layer between how the business talks and how the software is built, and a translation layer is where bugs and misunderstanding tend to hide.
A ubiquitous language is only consistent within a limited scope, and that is what connects it to boundaries. You cannot force the entire company to agree that "order" means exactly one thing, because the cart team and the fulfillment team mean different things by it and both are right.
The cart's order is a mutable basket of items with prices that can still change. Fulfillment's order is a frozen manifest of things to pick, pack, and ship. Trying to merge them into one universal "Order" model produces an object bloated with fields that only make sense half the time.
The place where one consistent language holds is the place where one model, and therefore one service, belongs. That place is the bounded context.
A bounded context is the boundary within which a particular model and its ubiquitous language stay consistent. Inside the context, "order" means one specific thing, the terms are unambiguous, and the model is coherent. Cross the boundary into another context and the same word can mean something else, governed by a different model.
A bounded context is the natural unit of decomposition, which is what makes it the heart of strategic DDD.
Each context owns its model, its language, and its data, and it shields all of that behind an explicit boundary. That description matches the definition of a good microservice almost word for word: a self-contained piece that owns its data and exposes a deliberate interface.
This is why the common advice is to identify your bounded contexts before you draw a single service.
The bounded context is where one consistent model lives, and it marks the seam the business divides along. The rest of this chapter covers how to find those seams and reason about what happens inside and between them.
The aggregate is the one tactical pattern that matters at the boundary level, because it decides what a single transaction is allowed to touch, and that constraint ripples out into how services and their data must be split.
An aggregate is a cluster of related objects that are treated as a single unit for the purpose of changes. One object in the cluster is the aggregate root, and the outside world is only allowed to reference the aggregate through that root.
An Order aggregate, for example, might contain the order itself as the root plus its line items as interior objects. You never reach in and modify a line item directly from outside. You go through the Order, which enforces the rules, such as the total always equaling the sum of its lines.
The rule that matters for distributed systems is this: an aggregate is the unit of transactional consistency. Within a single aggregate, you get ACID guarantees. You change the order and its line items together, in one transaction, and the invariants hold the entire time.
Across aggregates, you do not get that. Updating an Order and a separate Payment is two transactions, and between them the system is briefly inconsistent. You reconcile that gap with eventual consistency, typically through domain events, rather than wrapping both in one transaction.
This is where a tactical pattern shapes your architecture. If two pieces of data must always be consistent in the same instant, with no observable gap, they belong in the same aggregate, which means the same service. If they can tolerate a short delay before agreeing, they can live in different aggregates and therefore different services, coordinated by events.
Aggregate design becomes a direct input to where you are allowed to draw service boundaries. A boundary that cuts through a true consistency requirement will force you into distributed transactions, which the later data-management chapters show you want to avoid wherever you can.
Knowing what subdomains, contexts, and aggregates are does not tell you what they are for a specific business. You still have to discover them, and you cannot do that alone at a whiteboard, because the knowledge lives in the heads of the people who run the business.
Event Storming, created by Alberto Brandolini, is a workshop technique for pulling that knowledge out fast.
The format is deliberately low-tech: a long wall, sticky notes in a few colors, and the right people in the room, meaning both engineers and domain experts. You map the business as a flow of events and the things around them.
You start with domain events, written in the past tense because they record things that happened: "Order Placed," "Payment Charged," "Shipment Dispatched." Then you add the commands that cause them, the actors who issue those commands, the aggregates the commands act on, and the policies, the reactive rules that say "whenever this event happens, do that."
Laying all of this out along a timeline makes the structure of the business visible on the wall.
For decomposition, the value is that clusters emerge. Events and aggregates that belong together group up, and the gaps between those groups are strong candidates for bounded context boundaries. Where the language shifts, where one cluster's events stop and another's begin, you are looking at a context edge.
Event Storming gives you the raw material, the events and aggregates and the seams between clusters, that the strategic patterns then formalize into services. It also surfaces disagreements early: when two experts argue about what "Order Shipped" means, you have found a context boundary that a database-first design would have missed.
Once you have several bounded contexts, they still have to work together. A context map is the picture of how contexts relate, and it names the integration pattern for each relationship. Knowing these patterns lets you talk precisely about coupling between services instead of vaguely saying "they call each other."
| Relationship | What it means | When to use it |
|---|---|---|
| Shared Kernel | Two contexts share a small common model they jointly own | Rarely; only when sharing is cheaper than duplicating and both teams coordinate closely |
| Customer-Supplier | Downstream context's needs influence the upstream context's roadmap | When the upstream team can and will prioritize for the downstream |
| Conformist | Downstream simply accepts the upstream model as-is, no translation | When the upstream will not adapt and the cost of translating is not worth it |
| Anti-Corruption Layer | Downstream wraps the upstream behind a translation layer that protects its own model | When the upstream model is messy, legacy, or external and you must not let it leak in |
| Open-Host / Published Language | Upstream offers a well-documented, stable public interface for many consumers | When one context serves many others and needs a clean, versioned contract |
Two of these come up constantly. Use an anti-corruption layer whenever you integrate with a legacy system or a third party whose model you do not control.
Without it, that foreign model seeps into your code, and your clean context takes on the shape of the system you were trying to stay independent from. The ACL is a translation buffer that keeps your model yours.
The conformist relationship is its opposite choice: you decide the foreign model is good enough and adopt it, accepting the coupling to save the translation work.
Both are valid; the skill is knowing when each fits. Naming the relationship explicitly, "fulfillment is a conformist to the shipping provider, but we put an anti-corruption layer in front of the legacy inventory system," states the coupling precisely instead of vaguely.
DDD is not free. Event Storming workshops take real time, the modeling discipline has a learning curve, and the indirection that protects a complex domain is pure overhead in a simple one. Applied to the wrong problem, it produces ceremony without payoff, and part of using it well is knowing when to stop.
Skip or scale back DDD when the domain is simple and well understood. A CRUD-heavy product that mostly reads and writes records does not have rich business rules worth modeling carefully, and adding aggregates and context maps to it puts layers between a form and a table.
Small systems and small teams rarely earn the boundaries DDD draws, because there is no organizational complexity for those boundaries to manage. And a well-understood domain, where the right model is obvious and stable, does not need a discovery process to find it.
The rule is that DDD's investment pays off in proportion to domain complexity. Where the business logic is intricate, evolving, and central to the company's value, the strategic patterns are worth the effort because they keep that complexity from collapsing into a tangle.
Where the domain is thin, forcing DDD on it is a form of over-engineering. Recognizing the difference, and being willing to say "this domain does not need DDD," is as much a part of the skill as applying the patterns.
Domain-Driven Design belongs at the front of the decomposition part because it replaces guessed boundaries with discovered ones. The patterns split into two groups, and keeping them straight is the core of using DDD well.
The single thread tying all of this together is that the business, not the database schema, should dictate where the software divides.
That principle is the foundation for the next chapter, which takes the bounded context from a modeling concept to a concrete service boundary: the tests for whether a boundary is real, why one service per entity is a trap, and how to keep a context's edges clean as the system grows.
10 quizzes