Chapter 3: Domain-Driven Design and System Decomposition
Boundaries drawn from the org chart last a reorg; boundaries drawn from the domain last a decade.
Chapter 2 ended with a promise. Every architectural style, it said, depends on boundaries: a modular monolith needs modules, and a set of services needs seams between them. Boundaries, in turn, need a source, some principled reason why a line falls here and not there. We could take that reason from the technology, with a web layer here and a database layer there, or from the organization, with one module per team. The trouble is that both sources move under our feet. Frameworks and cloud services turn over every few years, and teams are reorganized at least as often, so boundaries drawn from either one must be redrawn each time they shift. The business domain moves far more slowly. People sold seats for concerts long before there was software to help them, and fans, seats, on-sales and refunds will still be Encore’s business long after today’s frameworks are forgotten. That makes the domain the most reliable source of boundaries we have.
Domain-driven design (DDD), the discipline Eric Evans set out in 2003, is the method for drawing boundaries from the domain. It is often mistaken for a catalog of code patterns, because so much of what is written about it concerns building blocks such as entities and aggregates (Section 3.2 takes those up). At heart, though, it is a discipline of listening. We listen for the places where a word changes meaning as it passes from one part of the business to another. One department’s “order” is another department’s “shipment request”: the same real-world thing, but carrying different data, obeying different rules, and mattering to different people. Where the meaning shifts, we draw a boundary. That is a different instinct from the one many of us learned, which is to study the database diagram, notice that two tables share a foreign key, and conclude that they belong together. The database diagram records how data happens to be stored today. The language records how the business actually thinks, and the business’s thinking is what lasts.
The idea is older than DDD, and it came from outside software. In Notes on the Synthesis of Form (1964), the architect Christopher Alexander asked why hard design problems so often defeat us. His answer was that such a problem is made of many requirements that pull on one another, more than any designer can hold in mind at once. The problem becomes tractable only when we find its clusters: groups of requirements that interact richly with each other inside the group and only sparsely with requirements outside it. Each cluster can then be worked on largely by itself. Alexander insisted that these clusters are already present in the problem, so the designer’s job is to discover them, not to impose a grouping that merely looks tidy. DDD applies the same argument to software. At Encore, the requirements for holding a seat during checkout, charging the fan, and releasing the seat if payment fails interact constantly, while they barely touch the requirements for logging a refund case in Support. The first group is a cluster; the second belongs to another. What DDD adds to Alexander is an instrument for finding the clusters: the business’s own language. When the words people use change meaning, we have usually crossed from one cluster into the next.
The chapter moves in two parts. Chapter 1 decomposed Encore by intuition: we looked at the business and sketched plausible parts. This time we use a method, so that each boundary comes with reasons we can state and defend (Sections 3.1 to 3.3). Then we take the more common case (Sections 3.4 and 3.5): an inherited system, tangled and profitable, that we must reshape while it keeps running, because the revenue it earns cannot pause while we work. That second case deserves our full attention. Most writing about architecture imagines that systems are born, designed on a clean sheet; most architecture work is done on systems that were inherited, with years of decisions already written into their code.
3.1 Strategic Design
Core, supporting, and generic subdomains
The first strategic act is a humbling one: admitting that your domain is not uniformly interesting. Every business is a bundle of smaller problems, which DDD calls subdomains, and only a few of them are the reason customers choose you over a competitor. The rest still have to work, but working well enough is all they need to do. DDD sorts subdomains into three kinds, and the sorting matters because it tells you where to spend your scarcest resources: your best engineers, your design attention and your appetite for building things yourself.
| Subdomain kind | Definition | Encore | Investment posture |
|---|---|---|---|
| Core | Where you beat competitors; changes constantly as you compete | Fair, bot-resistant on-sale admission; real-time seat inventory | Put your best engineers here, build it yourself, and evolve it aggressively |
| Supporting | Necessary and specific to your business, but not what makes customers choose you | Event catalog, organizer dashboards | Build it simply and keep it adequate |
| Generic | Every business needs it, and the industry has already solved it | Payments, email/wallet delivery, authentication (auth) | Buy a product or adopt a standard; innovate here only if it becomes your differentiator |
Figure 3.1 — Subdomain classification as capital allocation. The classic failure is inverted investment: a brilliant in-house auth system sitting beside an on-sale flow that falls over, which is like gold-plating the plumbing in a house whose roof leaks.
Read the Encore column as a set of bets. Fans do not pick Encore because its confirmation emails are elegant or because its login page is novel; they pick it because, when a stadium tour goes on sale, they believe they have a fair chance at a seat and that the seat they click is really theirs. That belief rests on two things: admission that keeps bots from crowding out people, and seat inventory that is correct in real time. Those are core, so they get Encore’s strongest engineers and custom code. The event catalog and the organizers’ dashboards are Encore’s own, and no vendor sells them in exactly the shape Encore needs, but they are not why fans choose Encore, so they are supporting and are built plainly. Payments, delivery and auth are problems that thousands of companies share and that vendors solve for a living, so Encore buys them.
A city planner would recognize the exercise. A city zones its land because each district deserves a different kind of attention. The civic center, the place that gives the city its identity, gets the celebrated architects and the stone. The warehouse district gets sound roofs and good roads, nothing grander, because sound and serviceable is what it needs. The water comes from a utility that already runs it well, because no city earns its reputation by digging its own pipes. Core, supporting and generic map neatly onto those three districts.
The analogy does break in one instructive place, and it is worth following to that point. Cities rezone slowly, over decades, while businesses rezone all the time. What is core today may become generic in five years, once some vendor packages it and sells it to everyone; at that moment your hand-built advantage turns into a cost you carry alone. So treat the classification as a map you will redraw, not a decision you make once.
The classification is also relative to the business doing it. Payments are generic for Encore, because Encore wins or loses on seats, not on how cards are processed. For a payment provider, the very same subdomain is core, because card processing is the thing it sells. There is no universal list of core subdomains; there is only your strategy, and the subdomains that serve it most directly.
Some subdomains sit on the border between two kinds, and auth is one. Encore buys its auth, as the table says. But identity is also one of the inputs to bot resistance, which is core: who signed up, when, from where, and with what history can help tell a fan from a script. So, in effect, Encore treats the subdomain in two parts. The login machinery stays bought and generic, while the sign-up and risk signals around it get closer review than anything else in the generic column. Border cases like this one earn their keep, because they force us to say exactly which part of a subdomain differentiates us, instead of classifying the whole thing by its name.
Bounded contexts
Listen to Encore’s people say “ticket,” and you will hear three different things. To Sales, a ticket is inventory with a price: a seat for a given event that can be held, sold or released. To the fan’s phone, it is a QR code that grants entry, something the door scanner checks once and then marks as used. To Support, it is a case number for a refund request, with a history of messages and a status. Now suppose we build one Ticket model to serve all three. It needs a price and a seat for Sales, a scannable code and an entry status for the phone, a case history and a refund status for Support. The result is a familiar monster: 40 fields, half of them nullable because each department fills in only its own, and a meaning that depends on who is asking. Every change requested by one department risks breaking the other two, and nobody can say what a Ticket is.
We have built that monster before, at enterprise scale. Many service-oriented architecture (SOA) programs of the early 2000s, whose larger story Chapter 2 told, began with a canonical data model: a single company-wide definition of Customer or Order that every system was supposed to share. They then spent years in committee trying to make one word mean one thing everywhere, and the word refused, because Billing’s customer and Shipping’s customer really are different things with different attributes and different rules. The lesson they paid for is where DDD begins: a model is useful precisely because it is partial. A model that leaves out what a given group does not need is easier to understand, easier to change and harder to get wrong.
That lesson gives us the central idea of strategic design. A bounded context is a boundary inside which each word has exactly one meaning and one model. Inside the Ticketing context, “ticket” always means an entry right; inside Support, it always means a case. Each context keeps a small, sharp model of its own instead of a share of one sprawling model. Evans called the shared vocabulary inside such a boundary the ubiquitous language: a promise that a word means the same thing whether the team is talking in a meeting, writing a document or naming a class in code. When the Ticketing team says “ticket,” the product manager, the tester and the Ticket type in the codebase all agree.
Bounded contexts also explain where our modules should come from. Chapter 2 gave Encore a modular monolith, and some of its modules may one day be extracted into services. Contexts give those modules, and any services cut from them, their semantic justification: a module boundary belongs where a vocabulary changes, not where a database table happens to end or a framework suggests a folder. Finding the boundaries is therefore an act of listening. You find one wherever two colleagues use the same word and mean two different things. A bounded context is, quite simply, the place where a word stops traveling: beyond that line, the word still exists, but it means something else, and the code must translate rather than pretend.
Context mapping
Drawing boundaries does not end the work, because the contexts must still talk to one another. When a seat is sold, Support needs to know about the ticket; when a fan pays, Orders & Payments must talk to an outside provider. How each pair of contexts relates is an architectural decision in its own right, and DDD gives the common relationships names, so a team can draw them on a context map and argue about them precisely:
Figure 3.2 — Encore’s context map, showing each context, its subdomain kind and the named relationship on each link. The anticorruption layer (ACL) around Payments matters most: the payment provider’s model of the world stops there and is translated into Encore’s terms, so moving to a new payment service provider (PSP) touches one translator in Orders & Payments and nothing else. On-Sale Admission is the Sale Gate of Chapters 1–2; Bot Screening, Notifications, and Sales Analytics are omitted for readability.
Four relationships cover most cases, and Encore’s map uses all four. A partnership ties two contexts’ success together, so their teams plan and change in step; it is expensive in coordination, so we keep it for links between core contexts, as Encore does between On-Sale Admission, Seat Inventory & Ticketing and Orders & Payments, which together make one sale. In a customer–supplier relationship, one context sits downstream of another, and the downstream context’s needs shape the upstream’s roadmap: Event Catalog supplies On-Sale Admission, so when admission needs a new field about an event, the catalog’s roadmap makes room for it. An open-host service publishes one stable, documented protocol that any consumer may use, instead of negotiating a custom link with each; Seat Inventory & Ticketing does this with its ticket events, which Support & Refunds simply subscribes to. An anticorruption layer sits at the edge of a context and translates a vendor’s or legacy system’s model into the context’s own terms, so that the foreign model stays at the border instead of colonizing yours.
That last word is meant literally. Without a translator, a payment provider’s ideas leak inward: its status codes appear in Encore’s order table, its notion of a “charge” shapes Encore’s notion of an order, and before long Encore’s core code is organized around a vendor’s API. Changing providers then means rewriting the core. With an ACL, Orders & Payments speaks only its own language, and one translator converts between that language and the provider’s. That is why the ACL is the pattern you will reuse most; expect to build one in nearly every year of your career, because every career includes vendors and legacy systems whose models you did not choose.
Notice that all four patterns name relationships between teams, not merely between pieces of code. A partnership is a promise to coordinate; customer–supplier says whose priorities win; an open-host service is a promise to keep a protocol stable for people you may never meet. That is why context mapping is, at bottom, about the politics between boundaries. Encore’s nine engineers are still one team, so for now the map records intent: it says how the contexts should relate once there are separate owners. The labels become binding on the day the team splits.
Context maps are discovered in workshops, not designed at desks, because the knowledge they need lives in the heads of the people who run the business. EventStorming, Alberto Brandolini’s workshop format, works well. Business people and engineers stand together at a long wall covered in paper and place orange sticky notes, each naming a domain event, something that happened in the business that someone cares about, such as SeatReserved, PaymentFailed or TicketIssued. They arrange the notes in time order and argue about the gaps. As the timeline fills, two kinds of evidence appear: clusters of events that clearly belong together, and places where the people at the wall start using a word differently. Both point at boundaries. One afternoon of this surfaces what requirement documents can hide for months, because a wrong sticky note is challenged on the spot by the person who knows. Some rooms find a timeline too abstract; for them, Domain Storytelling, in which people narrate concrete scenarios (“the fan opens the app, joins the queue, picks two seats…”) as pictographic stories, does the same work.
Before the workshop, large language models (LLMs) can do useful preparation. They can scan support tickets, documentation and code to flag words that seem to be used in two senses, and draft a first event timeline for the wall. Treat that draft as a hypothesis to test, not a finding. The model can notice that “ticket” means two things; only the domain experts at the wall know which sense of “ticket” the business actually depends on, and where the line between them should fall.
Recap. Classifying subdomains as core, supporting or generic tells us where to spend our best people, where to build simply and where to buy. A bounded context is a boundary of language, with one meaning and one model per word, and we find it by listening for the moment a word changes sense. A context map names how contexts relate (partnership, customer–supplier, open-host service, anticorruption layer), and the ACL keeps foreign models at the border.
Exercise 3.1. Find a word at your company that means different things to two departments. For each department, sketch the model the word implies: the handful of attributes and rules that department cares about. Then mark where the boundary falls: the point in a workflow, or the system, where one meaning hands over to the other.
3.2 Tactical Design for Architects
Aggregates as consistency boundaries
Section 3.1 drew boundaries between contexts. Now we step inside one context and ask how its model should be held together. DDD’s tactical patterns answer that question, and of all of them the one architects need most is the aggregate. An aggregate is a cluster of objects that we always change together, as a single unit, because together they guard an invariant: a business rule that must never be broken, not even for a millisecond while an update is halfway done. The invariant is the whole reason the aggregate exists. It tells us which objects belong inside, which belong outside, and how large the cluster has to be. A cluster of objects without such a rule behind it is not an aggregate at all; it is only a convenient grouping, and nothing would go wrong if we pulled it apart.
Encore’s most important invariant is easy to state: a seat is never sold twice. Think about what it takes to keep that promise. Reserving, releasing and selling seats all change the same thing, the state of an event’s seats, and while one of those changes is in progress, no other change may slip in and see a stale picture. So Encore treats all the seats of a single event as one aggregate, called EventSeating. That one choice settles three things at once. EventSeating is one transactional boundary, so every change to the seat map either commits in full or not at all. It is one lock scope, so two fans reaching for seat 14B at the same instant are handled one after the other, never side by side. And it is one source of truth, so there is exactly one place to ask whether seat 14B is free, and that place cannot disagree with itself.
The size of that boundary is a choice, and like every architectural choice it has a price. Notice first that “never sold twice,” taken alone, does not need the whole event. A guard on each seat would do: lock seat 14B while you sell it, and no one else can sell it at the same time. Encore draws the boundary around the entire seat map because it has other rules that span several seats at once. One rule forbids orphaned single seats, the lone empty seat stranded between two bookings that nobody will ever buy. You cannot check that rule by looking at one seat; you have to see its neighbors. Another rule is a per-fan ticket limit for each event, which means counting everything one fan holds across the whole map. Rules like these can be checked reliably only if everything they look at sits inside the same boundary, so the boundary grows to fit them.
The price of that larger boundary is contention. Because every seat write for an event passes through one lock scope, the writes queue up behind each other. For a club show that queue is invisible. For a stadium on-sale, where a stadium’s worth of fans arrives at the same moment, it becomes the bottleneck. The Sale Gate’s metering helps, because it lets fans in at a controlled rate instead of all at once, but it absorbs only part of the pressure; the writes that get through still wait for the same lock. Teams that hit this limit usually partition the aggregate, making each section of the venue its own aggregate. Writes to different sections then proceed in parallel. The cost is that rules which cross sections, such as the per-fan limit, can no longer be enforced inside one transaction. They become checks that run after the fact, catching a violation and correcting it, instead of preventing it outright. That is a real loss, and a team should take it knowingly.
A civil engineer would recognize this trade. A long bridge deck is not poured as one rigid slab; it is built in segments separated by expansion joints. Within a segment the concrete is rigid and moves as one piece. At the joints, neighboring segments can expand, contract and shift without cracking each other. The engineer chooses the segment length by asking what must stay rigid together. Make a segment too long and it cracks under its own internal stresses; in software we feel those stresses as lock contention, with every write in the segment waiting on every other. Make it too short and the rigidity we needed is gone; in software that means an invariant can be broken in the gap between two separately locked pieces. The analogy has a limit worth noting: the bridge’s joints only absorb movement, while the “joints” between aggregates must also carry information across, and that is the job of the domain events we turn to shortly.
Aggregates are consistency boundaries, not object graphs. The beginner draws the aggregate as everything reachable from Event: its seating, its orders, its refunds, its analytics. That design creates a lock the size of the business, in which issuing a refund could block a fan from buying a seat. The architect draws the smallest cluster that can guard the invariant. Everything else learns about changes afterward, through domain events.
The rule that follows is simple to say and hard to practice: an aggregate should be exactly as large as its invariant, and no larger. The difficult part is being honest about which invariants are real. A rule the business truly cannot live without breaking, like selling a seat twice, earns a place inside the boundary. A rule that is merely convenient, or one the business would happily see corrected a few seconds later, does not, and every such rule we wrongly treat as an invariant makes the aggregate bigger and the lock more contended.
If the aggregate is kept small, the rest of the system still needs to hear what happened inside it. Those changes travel as domain events, the second tactical concept architects need. A domain event is a record of something that has happened in the business, named in the past tense. When EventSeating sells a seat, it emits SeatSold, and the other contexts react in their own time. Ticketing hears it and issues the entry right. Notifications hears it and delivers the ticket to the fan’s wallet. Analytics hears it later still and updates its reports. (One thing does not wait for the event: the order commits in the same transaction as the seat, for reasons Section 3.3 explains.) Between aggregates, then, consistency is eventual. For a short while the seat is sold but the ticket is not yet in the wallet and the dashboard does not yet count it. That gap is not an accident or a flaw. It is a bargain the business struck knowingly: Encore’s finance team reconciles once a day and never expected every report to be true to the microsecond. Chapter 6 develops eventual consistency in full.
When tactical design pays off
Everything so far argues for tactical design, so it is only fair to say where it does not belong. Tactical DDD is a family of patterns: aggregates, entities (objects with a lasting identity, such as a fan), value objects (objects defined only by their values, such as a price), domain services, and ports and adapters (which keep the domain model separate from databases and outside systems). These patterns pay off only in complex domains. Chapter 1 used Brooks’s distinction between essential complexity, which belongs to the problem itself, and accidental complexity, which we add through our tools and methods. In a core domain full of tangled rules, such as seat allocation, the tactical patterns help tame the essential complexity. In a simple domain they are the accidental complexity, structure added to a problem that never needed it. Encore’s Notifications context mostly sends emails. A transaction script (a plain procedure that does the steps in order) and a queue serve it better than a domain model of aggregates and value objects.
The danger in applying tactical patterns everywhere is that it never looks like a mistake. Each pattern, taken alone, is good practice, so the supporting subdomains fill up with carefully built models, one well-made value object at a time, and the budget of time and attention that the core domain needed drains away into code that did not need the care. Nobody does anything wrong, and the core still ends up starved. In my experience, the mark of a senior designer is not knowing how to apply these patterns but knowing where to leave them out.
Recap. An aggregate is the smallest cluster of objects that guards a real invariant, and because of that it sets the size of our transactions, our locks and, eventually, our service boundaries. Making it larger buys more rules checked together at the price of contention; making it smaller buys throughput at the price of rules checked after the fact. Between aggregates, domain events carry the changes, under an eventual consistency the business has knowingly accepted. Tactical patterns earn their cost in complex core domains and should be left out of simple ones.
Exercise 3.2. Name the strongest invariant in a system you know: a rule that must never be broken, even briefly. Describe the smallest aggregate that could guard it, listing the objects inside it. Then describe what actually guards the invariant today (a table lock, a whole-database transaction, a single service) and say whether that boundary is bigger than it needs to be, and what the extra size costs.
3.3 Granularity — Forces That Split and Forces That Bind
Bounded contexts tell us where boundaries could go. They do not tell us how many of those boundaries should become separate deployable pieces, and that second question, how coarse or fine to cut, is the question of granularity. Two families of forces answer it. Disintegrators are reasons to push pieces apart: a split buys us something we cannot get while the pieces live together. Integrators are reasons to pull pieces together: a split would cost us something we cannot afford. Neither family wins by default, and most real architecture happens where the two collide over a particular boundary. The table below adapts the framework from Software Architecture: The Hard Parts, whose four integrators are database transactions, workflow, shared code, and data relationships. The rows on teams, deploy cadence, and the aggregate’s lock are this book’s additions.
| Disintegrators (split when…) | Integrators (merge when…) |
|---|---|
| pieces change at different rates | a workflow needs them in one transaction |
| one piece needs extreme scale (Sale Gate) | they call each other constantly (network latency on every call) |
| one piece must not take others down | they share data that cannot be teased apart |
| pieces need different security postures | the team is too small to operate more pieces |
| different teams own them | consistency demands one lock (the aggregate) |
| one piece deploys far more often | they share code that must change in lockstep |
Figure 3.3 — The granularity tug-of-war: forces that justify a split (left) against forces that resist it (right). Run every proposed split through both columns. A split that names no disintegrator, and is justified only by fashion or taste, loses to any integrator that applies.
The table is easiest to understand in use, so let us watch it settle a real Encore argument. In a design review, someone proposes splitting Seat Inventory from Order into separate services “for cleanliness.” That word should give us pause, because cleanliness appears in neither column. It is a feeling about how the diagram looks, not a force acting on the system. So we let the integrators speak first, and three of them object.
The first objection is about chattiness. Reserving a seat and creating the order are two steps of one user action: a fan picks seat 14B and buys it in a single breath, so the two components talk to each other on every purchase. Today that conversation is a function call inside one process, which costs next to nothing. Split them, and every reservation becomes a network call, with its added latency and its new ways to fail, on the busiest path Encore has: the on-sale, when thousands of fans are buying at once. Chapter 4 puts numbers on that cost.
The remaining objections concern the sale as a single transaction. A sale is one business transaction: either the seat is marked sold and the order exists, or neither is true. Inside one service, a single local database transaction guarantees that, because the database commits both changes together or rolls both back. Split the two, and no single database sees both changes any more. Encore would need a saga, the pattern Chapter 5 develops, in which each service commits its own step and, if a later step fails, runs a compensating action to undo the earlier ones: release the seat if the order cannot be created, for instance. A saga works, but it trades one atomic commit for a sequence of steps, a set of undo actions, and a window in which the system is briefly inconsistent. That is a heavy price to pay for tidiness.
Fairness demands that the disintegrators get their turn, and when they do, they have little to say. Different rates of change? Nearly every sales feature Encore has shipped touched both pieces, so they change together, not apart. Extreme scale? Neither piece needs more than the other, because the Sale Gate already meters admission upstream, and both see the same smoothed flow of buyers. Failure isolation? Separating them contains nothing: if Seat Inventory is down, Order has nothing useful to do, and the reverse is equally true, so a split would only add a second thing that can fail. No disintegrator applies, and three integrators do. The two pieces stay together.
The decision does not end with “no.” The proposal is archived, with its reasoning, as a rejected architecture decision record (ADR), the habit Chapter 1 started. That record matters more than it seems. The next time someone suggests the same split, perhaps years later and for the same reasons, the team can read why it was turned down and ask a sharper question: has any of those forces changed since?
The table decides whether a split should happen. Whether a split that does go ahead will survive is decided by Conway’s law, which we met in Chapter 1: systems tend to take the shape of the communication structure of the organization that builds them. Consider what a boundary costs when it cuts across teams. If shipping one feature requires changes on both sides, and each side belongs to a different team, then that feature needs two backlogs, two sprint plans, and a negotiation every time. People do not tolerate that friction for long. Within a year, the organization tends to remove it, by merging the services, by letting one team quietly take over both, or by routing work around the boundary until it exists only on paper, whatever the architecture diagrams still say.
Water held behind a dam is a fair picture of that pressure. The water does not argue with the dam; it simply finds every crack, and it widens the ones nobody is watching. Coordination cost behaves the same way, seeping through the parts of a boundary that no team owns or defends. The analogy has its limit, of course: a dam is meant to hold back water forever, while some coordination across teams is healthy and expected. What does not last is a boundary that forces coordination on every change. The lesson cuts in two directions. The org chart is a poor source of boundaries, because organizations reorganize far more often than the business domain changes. Yet the org chart always gets a vote on whether the boundaries we draw will last.
If the organization gets a vote, the practical response is to shape teams to fit the boundaries we want, so that each boundary we draw from the domain also falls between teams that can work independently across it. This is Conway’s tendency used deliberately, in reverse, and it is a later idea than Conway’s own: the “inverse Conway maneuver,” which Chapter 5 takes up. For now, the point is that team structure is not something to sort out after the decomposition is done. It belongs among the decomposition forces from the very beginning, alongside rates of change, scale, and transactions.
Recap. Granularity is a balance between forces that justify a split and forces that resist it. Every proposed split must name the disintegrator that justifies it and still hold up against the integrators. Encore keeps Seat Inventory and Order together because three integrators apply and no disintegrator does. And because of Conway’s law, team structure then decides whether a split that does go ahead will last.
Exercise 3.3. Take the most recently split (or proposed-split) service at your work. Write down which disintegrator from Figure 3.3 justified the split, or note that none did. Then list the integrators that applied but were ignored, and describe what each one has cost since, or would cost if the split went ahead: added latency, a saga where a transaction used to be, coordination across teams, or something else.
3.4 Decomposing Existing Systems
The economics of decomposition
Encore is fortunate: it is young, so we have been able to draw its boundaries on a clean sheet. Most of us will not be so lucky. The system you are more likely to be handed is nine years old, profitable, and so critical to the business that nobody dares switch it off. Before asking how to decompose such a system, we should ask whether to, because decomposition is an investment with a real price, and it pays back only when it removes a real cost.
What counts as a real cost? The monolith should measurably block the business, and you should be able to point at the measurement. Deploys collide every week, because several teams share one release train and one team’s broken change holds up everyone else’s. One component’s scaling needs starve the rest: to give checkout enough capacity for a big on-sale, you would have to run many more copies of the whole application, reporting module and all, because you cannot scale one part on its own. Teams sit idle in merge queues, waiting their turn to land a change. Each of these is a cost you can count in hours or in money, and each is the kind of problem that separating the pieces actually solves. Notice that each is also one of Section 3.3’s disintegrators wearing work clothes.
The bad reasons are just as recognizable. Don’t decompose because the code is ugly; ugly code inside one process is a refactoring problem, and splitting it only spreads the ugliness across a network. Don’t decompose because a hiring slide promises “microservices.” And don’t decompose merely because the monolith is old. A system that has been profitable for nine years has survived nine years of the business changing its mind, which is strong evidence that its structure works better than its reputation suggests. Age is not a defect; it is a track record.
I have sat in more than one meeting where a rewrite was pitched as a migration. The giveaway is simple: ask how much coordination pain the split would remove, in minutes per week, and nobody can answer. Without that number, the project is not solving a problem; it is pursuing a picture. And the picture rarely survives contact with the code. If the monolith is tangled, with modules reaching into each other’s internals and sharing tables at will, then cutting it into services does not untangle anything. You get tangled services with a network in between. Every old problem remains, because the dependencies are still there, and you add three new ones: latency on every call that used to be a function call, partial failure when one service is down and its callers are not, and an operations bill for running, monitoring and paging on all those new moving parts.
History has a name for this temptation. Fred Brooks, in The Mythical Man-Month (1975), described the second-system effect: when a designer builds a second system, they tend to overload it with every idea they had to defer from the first, and the result is bloated by their ambition. The grand rewrite is the second-system effect at the scale of a whole organization. Everyone has a list of what they would do differently, and the rewrite becomes the place where every list is finally honored at once.
Between leaving the monolith alone and extracting services sits a third option, and it is the right one more often than either: modularize in place. Carve the monolith into Chapter 2’s modular-monolith shape, so that each bounded context becomes a module with a public interface and private internals, all still running in one process and deploying as one unit. Then enforce those module boundaries in the build, so that a change reaching into another module’s internals fails the build rather than slipping through code review. ArchUnit, Spring Modulith, and Packwerk are common tools for this. Only later, if a disintegrator still applies, do you extract services, and you extract them along the boundaries that have held.
The reason for this order is that a boundary drawn on a whiteboard is only a hypothesis. Inside one process, a wrong boundary is cheap to fix: you move a class, rename a package, redeploy. Across a network, the same mistake means changing an API contract, coordinating two releases and migrating data. So we let the cheap environment test the hypothesis first. A boundary that has survived a year of real change inside one process, with features shipping and nobody needing to break it, has earned the right to become a network boundary.
The strangler fig and its companion patterns
When decomposition is justified, the main pattern is the strangler fig. Rather than building a replacement in secret and switching over on one terrifying weekend, you grow the new system around the old one. You move traffic over one capability at a time, and you let the old code fall out of use as each of its jobs is taken away. Martin Fowler named the pattern in 2004 after the strangler figs of the Queensland rainforest. A fig seed lodges high in a host tree and sends roots down around the trunk; over the years the fig thickens until it stands on its own, long after the host tree inside it has gone.
Figure 3.4 — The strangler fig: a routing facade sends each capability’s traffic to either the old system or the new one. Any capability behind the facade can move without users noticing, and changing a routing rule reverses each move, so rollback is a configuration change.
The mechanism that makes this work is the routing facade at the front. Every request passes through it, and for each capability it holds a rule that says where that capability now lives. Imagine, for illustration, that Encore had an older ticketing system to replace. On the first day, every rule points at the old system. When the new event-listing service is ready, one rule changes, and requests to browse concerts start going to the new code while checkout, refunds and everything else stay where they were. A fan browsing concerts cannot tell that anything changed. Weeks later, another rule moves another capability, and so on until the old system receives no traffic at all and can be retired.
The botanical image teaches the essential point well: the host keeps carrying load while the replacement grows, so at no moment is any part of the business left unsupported. But the analogy breaks in one place, and the break matters more than the resemblance. A real fig cannot hand a branch back to its host. Our facade can. If the new event-listing service misbehaves during an on-sale, we change its routing rule back, and within moments fans are browsing on the old system again. Every move is a rule we can revert. That reversibility is what turns a migration from a single large gamble into a sequence of small, recoverable steps, each of which can fail without becoming a disaster.
Two companion patterns cover the risky middle of a migration, where old and new code must coexist. The first is branch by abstraction. Sometimes you cannot intercept the traffic at the edge, because the capability you want to replace is called from deep inside the monolith rather than through an external request. Then you create the seam inside the code instead: introduce an interface in front of the old implementation, change every caller to use that interface, build the new implementation behind the same interface, and use a toggle to choose which one runs. The interface plays the role that the routing facade plays at the edge, and the toggle plays the role of the routing rule, so the move stays just as reversible.
The second is parallel run, which fits when correctness matters more than speed. Both implementations execute on the same real inputs, their results are compared, and the old one stays authoritative: its answer is the one the system acts on, while the new one’s answer is only recorded and checked. Every disagreement is either a bug in the new code or a hidden rule in the old code that nobody had written down, and either way you want to find it before customers do. A new seat allocator at Encore is a good example. A wrong seat assignment means two fans holding tickets for the same seat, so the new allocator should run beside the old one for two weeks of real on-sales, with every disagreement investigated, before anyone trusts it to make the decision.
Recap. Decompose only for measured business pain, never for looks, and consider modularizing in place first, so that boundaries prove themselves where mistakes are cheap. When extraction is justified, a strangler fig with a routing facade makes migration incremental and, unlike its namesake, reversible. Branch by abstraction brings the same reversibility inside the code, and parallel run lets the new implementation prove itself before it is trusted.
Exercise 3.4. For a monolith you know, name the first capability you would extract. Which disintegrator from Section 3.3 justifies extracting it, and what would its routing rule look like (which requests it matches, and where it sends them before and after the move)?
3.5 Decomposing the Database
Splitting the code is the easier half of a migration. Code can be moved behind a routing facade one capability at a time, as Section 3.4 showed, and a routing rule undoes each move. The shared database offers no such convenience, and it is where migrations most often stall. The reason is that the database is the part of the monolith that remembers every shortcut ever taken. Over the nine years of a system like the one Section 3.4 described, whenever one module needed something another module owned, the quickest path was rarely to ask that module through its interface; it was to read the other module’s table directly, or to join against it in a query. Each such shortcut was harmless on the day it was written. Together they mean that the tables no longer belong to anyone in particular, and you cannot pull one capability’s data out without discovering who else has been leaning on it.
A database in that condition will not yield to a single bold stroke. It yields only to a sequence of smaller steps, each of which leaves the system working and each of which can be undone. To make the steps concrete, imagine extracting a Notifications capability from a long-lived monolith, one in which notification preferences, delivery history and fan contact details all sit in tables that the rest of the code reads freely.
The first step is to give each bounded context its own schema: a named group of tables inside the same database server, owned by one context. Nothing moves yet, and the data stays on the same machine. What changes is ownership. The Notifications tables now live in a notifications schema, and any code outside the Notifications module that touches them stands out as a visible violation instead of an invisible habit. This step sets ownership on paper, which is less than it sounds and more than nothing: it turns a vague sense that “everyone uses these tables” into a concrete list of offenders.
The second step removes those offenders by replacing every cross-context join. Suppose the order-confirmation code joins the orders table to the fan’s notification preferences in one SQL query. That query cannot survive the split, because once the two tables live in different databases, no single query can reach both. There are two ways to replace it. The order code can call a Notifications API and ask for the preferences it needs, which keeps the data in one place at the cost of a network call. Or Notifications can publish its changes, and the order side can maintain a replicated read model: a local, read-only copy of just the fields it needs, kept up to date from those published changes. The read model avoids the network call but accepts that the copy may lag the original by a little, the same eventual-consistency bargain Section 3.2 described between aggregates. Which to choose depends on how fresh the data must be and how often it is read.
Only after the joins are gone does the third step move the data itself. Here the workhorse is change data capture (CDC). Most databases keep a transaction log, an ordered record of each insert, update and delete it commits, which they use for their own recovery and replication. CDC reads that log and streams each change to another destination as it happens. Pointed from the old Notifications tables to the new Notifications store, CDC keeps the new store a faithful, continuously updated copy while the old one is still in charge. That lets us migrate reads gradually: first a report, then a background job, then the fan-facing screens, checking at each stage that the new store returns the same answers as the old.
Engineers who strengthen the foundations of an occupied building, a job called underpinning, face the same problem we do. The building cannot be emptied, so they replace the foundation one section at a time, and they follow one rule above all: at every moment, each part of the load rests on exactly one support. A wall resting on the old footing and the new one at once, with neither clearly in charge, is a wall that can crack as the two settle differently. Our migration follows the same rule. At every step, exactly one store is authoritative for each piece of data, meaning it is the one place where writes happen and the one whose answer wins if the two ever disagree. During the gradual migration of reads, that is still the old store; the new store is only a copy.
The tempting shortcut breaks that rule. It is the application dual-write: change the code so that every save writes to both the old store and the new one. It looks simple, but the two writes are not one transaction. If the first succeeds and the second fails, because of a timeout, a crash between the two calls, or a deploy at the wrong moment, the stores now disagree, and nothing in the system notices. A fan’s updated email address lands in one store and not the other. Multiply that by months of traffic and the two stores drift apart quietly, until the day someone switches over and finds that the new store is subtly wrong in ways nobody can reconstruct.
So instead of writing twice, we move authority once. When the new store has been proven on reads, we flip write ownership in a single step: from that moment, writes go to the new store, and it becomes authoritative. To make the flip reversible, we keep the old store fed in the opposite direction. Either CDC now runs from the new store back to the old one, or the new service uses an outbox (Chapter 6): it records each change, together with an event describing it, in the same local transaction, and a relay later publishes those events to whoever needs them, including the old store. Either way, the old store stays current. If the new service misbehaves after the flip, we flip back, and the old store is ready to resume as the authority without losing a single write made in the meantime. That is the database’s equivalent of the routing rule that makes a strangler fig reversible.
Expect unpleasant discoveries along the way. A monthly finance report that joins a dozen tables across every context. A consumer nobody declared, such as a partner’s nightly export that reads the fan contact table directly. A “temporary” direct-table access added in 2019 to fix an urgent bug, still running years later. None of these appears in the architecture diagrams, because none of them went through an interface; each is a dependency the database had been keeping quietly on everyone’s behalf. Each must be found and untangled, by giving it an API, a read model or a feed of its own, before the split can finish, and each one found late delays the flip. This is why Section 3.3’s integrator for data that “cannot be teased apart” deserves its full weight before you split. If the honest inventory of a capability’s tables turns up a long list of readers like these, that is the integrator speaking, and it may be telling you to keep those pieces together.
Recap. The database splits last, and in order: first a schema per context, which makes ownership visible; then APIs or replicated read models in place of cross-context joins; then CDC to copy the data while reads move over. At every step exactly one store is authoritative. Avoid application dual-writes, which drift silently, and instead flip write ownership in one step, with CDC or an outbox feeding the old store in reverse so the flip can be undone without losing data.
Exercise 3.5. For the capability you chose in Exercise 3.4, list the tables it shares with the rest of the monolith. For each table, name everyone else who reads or writes it, including reports, batch jobs and anything outside the code base. Then state which store would be authoritative on the day you flip writes.
Kata: Harvest
This kata asks you to do, on someone else’s system, everything the chapter did on Encore’s. Encore let us work forward from a young domain; Harvest hands us the more common situation of Section 3.4, a running system that is old enough to be tangled and too important to switch off. Read the brief the way Section 3.1 taught us to listen to Encore’s people: for the places where the business tells you what it values and where words change meaning.
Your brief: “Harvest.” A farm-to-restaurant marketplace, eight years old, built as one Ruby on Rails monolith and worked on by 26 engineers in four teams. Its week runs on a fixed rhythm: farmers list that week’s harvest, restaurants place their orders by Thursday, delivery routes are planned on Friday, and the trucks roll out at 4 a.m. on Saturday. Deploys are frozen from Wednesday to Saturday, because nobody is sure what touches routing, and a bad change in that window could leave a restaurant without its Saturday delivery. Three people want three different things. The delivery-routing team wants to rewrite routing in place, inside the monolith. The chief technology officer (CTO) wants “microservices like Uber.” The chief financial officer (CFO) wants neither to cost anything. Logistics, getting the right produce to the right kitchen on time, is what sets Harvest apart from its competitors; listings are commodity software that any marketplace has; payments are handled by Stripe.
Deliverables:
- Subdomain classification — sort Harvest’s subdomains into core, supporting and generic, as Figure 3.1 did for Encore, and give each one sentence of investment posture: where the best engineers go, what is built simply and kept adequate, and what is bought.
- Context map — draw Harvest’s bounded contexts and name the relationship between each connected pair, using the four relationships of Section 3.1 (partnership, customer–supplier, open-host service, anticorruption layer). Include at least one ACL, and justify it by saying whose model it keeps out and what would leak in without it.
- Granularity verdicts — take three proposed splits and run each through the Figure 3.3 force table, naming the disintegrators that argue for it and the integrators that argue against it. At least one of the three must be rejected, and the verdict should show why, the way Section 3.3 showed why Encore’s proposed split lost to its integrators.
- Migration plan — choose the first capability to extract, lay out the strangler sequence that follows it, describe the database strategy (Section 3.5’s order of schema, then API, then data), and say what “reversible” means at each step: what you would do to undo it if it went wrong.
Rubric:
| Criterion | The question to ask yourself |
|---|---|
| Language evidence | Is each boundary justified by a shift in what a word means (what “order” means to a restaurant, a farmer and a route planner, say), and not by how the tables happen to be arranged? |
| Investment sanity | Do the strongest engineers go to the core subdomain, and are the generic subdomains bought instead of built? |
| Force honesty | Did any split survive without naming a disintegrator that justifies it? If so, it is fashion, and it should not survive. |
| Reversibility | At every migration step, what is the undo, and is it a change to a routing rule or a rewrite? |
Where you now stand
You can now find boundaries the business will respect, because they follow its language. You can size the pieces by the forces that act on them, splitting only where a disintegrator demands it. And you can reshape a running system without stopping it, one reversible step at a time. What we have let ourselves ignore so far is what happens between the pieces once a network separates them. Inside one process a call is fast, it either happens or it doesn’t, and a single database transaction can keep several changes together. Across a network, every call takes time, any call can fail halfway with the caller unsure whether it worked, and the single transaction is gone. Chapter 4 takes up that physics, and the engineering that copes with it.
References
- Matthew Skelton, Manuel Pais — Team Topologies, 2nd ed.. IT Revolution, 2025.
- Neal Ford, Mark Richards, Pramod Sadalage, Zhamak Dehghani — Software Architecture: The Hard Parts. O’Reilly, 2021.
- Vlad Khononov — Learning Domain-Driven Design. O’Reilly, 2021.
- Alberto Brandolini — Introducing EventStorming. Leanpub.
- Stefan Hofer, Henning Schwentner — Domain Storytelling. Addison-Wesley, 2021.
- Sam Newman — Monolith to Microservices. O’Reilly, 2019.
- Martin Fowler — StranglerFigApplication. martinfowler.com — free online.
- Martin Fowler — Refactoring, 2nd ed.. Addison-Wesley, 2018.
- Scott Wlaschin — Domain Modeling Made Functional. Pragmatic Bookshelf, 2018.
- Vaughn Vernon — Implementing Domain-Driven Design. Addison-Wesley, 2013.
- Michael Feathers — Working Effectively with Legacy Code. Prentice Hall, 2004.
- Eric Evans — Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003.