Quick Navigation
- Start here — Why decomposition matters
- Strategies for decomposition
- Defining boundaries
- Coupling and cohesion
- Domain-Driven Design basics
- Common mistakes
- Putting it all together
- Cheat sheet
Before we start — the one thing to hold onto
Every system that grows large enough becomes unmaintainable — unless someone deliberately breaks it into parts. I've seen teams start with a clean monolith and end up with something where every change requires understanding the whole thing. The cost compounds quietly, then all at once.
Decomposition is the act of deciding what belongs together, what belongs apart, and where the seams between parts should run — before the code tells you by breaking.
The parts you choose shape everything that follows: which teams can work independently, which changes are safe, and which changes ripple across the whole system.
Keep it in mind.
1. Why decomposition matters — The cost of one big thing
A system built as one undivided whole feels faster to start. It becomes impossibly slow to change.
Here's what happens without decomposition. Every part of the system can reach into every other part. A change in one area breaks something unrelated. Two teams block each other because they touch the same code. Deploying one feature requires deploying everything.
The cost compounds:
- Change velocity drops — every change requires understanding the whole
- Team autonomy vanishes — teams cannot work independently
- Risk increases — a failure anywhere can cascade everywhere
- Testing becomes exhaustive — you cannot test a part in isolation
- Deployments become all-or-nothing — one release, one risk
flowchart LR
S1["🟢 Small System\n1-3 teams, manageable"] -->|"Grows without\ndecomposition"| S2["🟡 Medium System\n4-10 teams, slowing"]
S2 -->|"Changes conflict\ndependencies multiply"| S3["🔴 Large System\n10+ teams, crippling"]
S3 -->|"Every change\nis high-risk"| S4["💀 Accidental Monolith\nNothing moves fast"]
Here's the part that catches teams out: they delay decomposition because "we'll do it later when we know the domain better." But the cost of decomposition rises as coupling deepens. The seams you don't choose deliberately get chosen accidentally — by whoever writes the code first.
Undecomposed systems don't stay simple — they become accidental monoliths where every change is expensive.
Try it yourself — The decomposition audit
Think of your system. Answer these questions honestly:
| Question | Your answer |
|---|---|
| Can one team deploy without coordinating with another? | |
| Does a change in one area ever break something unrelated? | |
| Can a new engineer understand one part without learning the whole thing? | |
| Do you have a "common" or "shared" service that everything depends on? |
If you answered "no" to the first two or "yes" to the last two, your system is telling you it needs decomposition. Write that down before moving on.
2. Strategies for decomposition — By domain, capability, data, team
There are many ways to break a system apart. The strategy you choose determines what kind of boundaries you get.
Four common strategies:
By business domain — split along the natural boundaries of the business. Orders, payments, inventory, shipping. Each domain owns its logic and its data.
By capability — split along what the system can do. Authentication, notification, reporting, search. These are shared capabilities that serve multiple domains.
By data — split along data ownership. Who owns what data, who can read versus write. The boundary follows the data, not the function.
By team — split along organisational boundaries. What can a single team own end-to-end? Conway's Law in action.
No single strategy is always right. Most real systems use a mix — domain boundaries at the top level, capability boundaries within domains.
flowchart TD
Q{"What's the primary\naxis for splitting?"}
Q -->|"Business areas\nare distinct"| DOMAIN["By Domain\nOrders, Payments, Inventory"]
Q -->|"Shared functions\nserve multiple areas"| CAPABILITY["By Capability\nAuth, Notifications, Search"]
Q -->|"Data ownership\nis the constraint"| DATA["By Data\nWho owns what, who reads/writes"]
Q -->|"Team autonomy\nis the driver"| TEAM["By Team\nWhat can one team own end-to-end"]
Here's the practical approach I use: start with domain decomposition — it aligns with how the business thinks. Within each domain, identify capabilities — the functions that domain needs. For each capability, identify data ownership — who reads, who writes. Then align team boundaries to the resulting structure.
The best decomposition strategy matches the axis of change — split where things change independently.
Try it yourself — The axis of change test
Think of three recent changes in your system. For each one:
| Change | What else had to change? | Could this have been isolated? |
|---|---|---|
| e.g. Changed payment provider | Checkout flow, reporting, reconciliation | Yes — if Payments was its own boundary |
If your answer to the second column keeps pulling in unrelated functionality, your boundaries aren't drawn along the axis of change.
3. Defining boundaries — What belongs inside vs outside
Once you decide to decompose, you must decide exactly where the boundaries run. Getting the boundary wrong is worse than not decomposing at all.
A good boundary groups things that change together and separates things that change independently. Inside the boundary, components are tightly coupled — they share data, call each other directly, and are deployed together. Between boundaries, components communicate through stable interfaces.
The boundary defines what is inside, what is outside, what crosses the boundary — the messages, events, or API calls that flow between components — and what is shared.
flowchart TD
BOUND["Boundary"] --> INSIDE["Inside\nTightly related\nChanges together"]
BOUND --> OUTSIDE["Outside\nOther components\nStable contracts"]
BOUND --> CROSSES["What crosses\nAPIs, events, messages\nVersioned schemas"]
BOUND --> SHARED["What's shared\nCommon data or services\nMultiple components need"]
Here's the part that catches people out: a boundary isn't a line on a diagram. It's a commitment that what is inside can change without breaking what is outside. If you can't keep that promise, the boundary is wrong.
The boundary contract is simple. Outside sees the interface — API, events, messages. Inside contains the implementation — code, data, logic. What crosses: defined messages with versioned schemas. What does NOT cross: internal data models, implementation details.
A boundary is not a line on a diagram — it is a commitment that what is inside can change without breaking what is outside.
Try it yourself — The boundary checklist
Pick two components in your system. Run them through the checklist:
| Question | Your answer |
|---|---|
| Does this functionality change when the other changes? | If yes, they belong in the same boundary |
| Can this be deployed independently? | If yes, the boundary is real; if no, it's aspirational |
| Does this component need the other's internal data? | If yes, the boundary is in the wrong place |
| Is the interface between them stable? | If no, the boundary will create friction |
Any "no" to the second column means your boundary needs work.
4. Coupling and cohesion — The two forces every decomposition must balance
Every decomposition decision creates two opposing forces. Pull things apart and you reduce coupling but risk losing cohesion. Push things together and you increase cohesion but risk tight coupling.
Coupling is how much one component depends on another. High coupling means a change in one forces a change in the other. Cohesion is how well the things inside a component belong together. High cohesion means the component has a clear, unified purpose.
The goal: high cohesion within components, low coupling between components.
Good decomposition:
[Orders] ---loose coupling--- [Payments] ---loose coupling--- [Inventory]
(high cohesion) (high cohesion) (high cohesion)
Bad decomposition:
[Orders + half of Payments] ---tight coupling--- [other half of Payments + Inventory]
(low cohesion) (low cohesion)
There are types of coupling, from worst to best. Content coupling — one component reaches into another's internals. Reading another service's database table directly. Common coupling — components share global data. Both read/write the same config file or shared table. Control coupling — one component passes control flags to another. "Process this, but skip the validation step." Stamp coupling — components share a data structure but use different parts. Data coupling — components communicate through simple parameters. Clean API call with request/response.
And types of cohesion, from worst to best. Coincidental — elements grouped by accident. Logical — elements grouped by category — "all validators." Functional — every element contributes to a single, well-defined task.
If you can only remember one design heuristic for decomposition, remember this: maximise cohesion, minimise coupling.
High cohesion inside, low coupling outside — the decomposition mantra.
Try it yourself — The coupling audit
List three dependencies between components in your system. Rate each one:
| Dependency | Type of coupling | Could it be improved? |
|---|---|---|
| e.g. Orders reads Payments database directly | Content coupling — worst kind | Yes — use API or events instead |
If you have any content or common coupling, your boundaries are leaking. Fix those first.
5. Domain-Driven Design basics — Bounded contexts and ubiquitous language
The business uses words like "customer," "order," and "product." But those words mean different things in different parts of the business. If your system uses the same model for everything, it will fight itself.
Domain-Driven Design provides two foundational tools for decomposition.
Bounded context — a boundary within which a model has a specific, consistent meaning. "Customer" in the sales context is different from "Customer" in the support context.
Ubiquitous language — the shared language between developers and domain experts within a bounded context. Everyone in the context uses the same words to mean the same thing.
flowchart TD
BIZ["Business Domain\nSales · Support · Fulfilment"]
BIZ --> BC1["🛒 Sales Context\n'Customer' = prospect/buyer\n'Order' = cart/checkout"]
BIZ --> BC2["🎧 Support Context\n'Customer' = account holder\n'Order' = complaint/ticket"]
BIZ --> BC3["📦 Fulfilment Context\n'Customer' = destination\n'Order' = shipment/parcel"]
Here's what catches teams out: they create a single "Customer" object that tries to serve every context. It becomes a God object — bloated, confusing, and impossible to change without breaking something.
A bounded context is a semantic boundary — inside it, words have one meaning; outside, they may have another. Same word, different meaning → different bounded contexts.
Bounded contexts integrate through patterns. Customer-Supplier — upstream context provides, downstream consumes. Conformist — downstream accepts upstream model as-is. Anti-corruption Layer — downstream translates upstream model to its own. Shared Kernel — two contexts share a small, agreed model.
A bounded context is a semantic boundary — inside it, words have one meaning; outside, they may have another.
Try it yourself — The language test
Pick a word your business uses — "customer," "order," "product." Now ask three people in different roles what it means:
| Role | Their definition | Same or different? |
|---|---|---|
| Sales | ||
| Support | ||
| Fulfilment |
If the definitions differ, you have multiple bounded contexts — and your system should reflect that.
6. Common decomposition mistakes — And what they cost
Getting decomposition wrong is expensive — and the mistakes are predictable. They appear again and again.
Mistake 1: Decomposing too early. Splitting into microservices before understanding the domain creates boundaries in the wrong places. Moving a boundary is harder than creating one.
Mistake 2: Decomposing too late. Waiting until the monolith is so coupled that decomposition requires a rewrite. The cost rises exponentially with delay.
Mistake 3: Decomposing by technology layer. Splitting into "UI service," "API service," "database service" instead of by business capability. Every feature change touches all layers — coupling is maximised, not minimised.
Mistake 4: Shared databases across boundaries. Two components share one database schema. The boundary is an illusion — any schema change breaks both components.
Mistake 5: Chatty boundaries. Components call each other hundreds of times per request. The network becomes the bottleneck and deployment becomes impossible because every change requires coordinated releases.
Mistake 6: God components. One component that does too much — the "common" or "shared" or "core" service that everything depends on. It becomes the bottleneck for every team.
flowchart TD
MISTAKES["Decomposition Mistakes"]
MISTAKES --> M1["Too early\nBoundaries wrong"]
MISTAKES --> M2["Too late\nRewrite required"]
MISTAKES --> M3["By layer\nMax coupling"]
MISTAKES --> M4["Shared DB\nFake boundary"]
MISTAKES --> M5["Chatty\nNetwork bottleneck"]
MISTAKES --> M6["God component\nSingle bottleneck"]
M1 --> FIX["Fix: Wait until coupling hurts,\nthen decompose by domain"]
M2 --> FIX
M3 --> FIX
M4 --> FIX
M5 --> FIX
M6 --> FIX
Most decomposition failures aren't wrong technology — they're wrong boundaries. The six deadly sins: too early, too late, by layer, shared DB, chatty, god component.
When you do get it wrong, the Strangler Fig pattern is your friend. Instead of rewriting, incrementally extract capabilities from the monolith. Identify a capability that can be isolated. Build the new component alongside the old. Redirect traffic to the new component. Remove the old implementation. Repeat.
Most decomposition failures are not wrong technology — they are wrong boundaries.
Try it yourself — The mistake inventory
Which of these mistakes exist in your system right now?
| Mistake | Evidence | Cost |
|---|---|---|
| e.g. Shared database | Orders and Inventory share the same schema | Can't deploy Orders without testing Inventory |
Pick one. What would it take to fix it? Be specific — not "separate the databases," but "extract Inventory data model, create API, migrate consumers one at a time, drop shared tables."
Putting it all together
Here's the complete picture in one diagram. This is the mental model worth internalising.
flowchart TD
BIZ["Business Domain\nCapabilities and processes"] --> DDD["Domain Analysis\nBounded contexts\nUbiquitous language"]
DDD --> DECOMP["Decomposition\nDomain · Capability · Data · Team axes"]
DECOMP --> BOUND["Boundary Design\nHigh cohesion inside\nLow coupling outside\nStable interfaces"]
BOUND --> INT["Integration Patterns\nAPIs · Events · Messages"]
BOUND --> DATA["Data Ownership\nEach context owns its data\nNo shared schemas"]
BOUND --> TEAM["Team Alignment\nOne team per boundary\nEnd-to-end ownership"]
INT --> DEPLOY["Independent Deployment\nSeparate release cycles\nSeparate scaling"]
DATA --> DEPLOY
TEAM --> DEPLOY
The foundation:
Decompose along the axis of change — what changes together stays together; what changes apart is separated. Maximise cohesion, minimise coupling — high cohesion inside components, low coupling between them. Boundaries are commitments — a boundary says "what is inside can change without breaking what is outside." If you can't keep that promise, the boundary is wrong.
Cheat Sheet — All the key terms
| Concept | One-Line Memory | What It Really Means |
|---|---|---|
| Decomposition | Choosing seams before code chooses them | Breaking a system into manageable, independent parts |
| Domain decomposition | Split by business area | Aligns with how the business thinks |
| Capability decomposition | Split by function | Aligns with shared services and cross-cutting concerns |
| Coupling | How much components depend on each other | Lower is better between components |
| Cohesion | How well things inside belong together | Higher is better within components |
| Bounded context | Semantic boundary with consistent meaning | Same word can mean different things in different contexts |
| Ubiquitous language | Shared vocabulary within a context | Developers and domain experts use the same words |
| Distributed monolith | Fake decomposition — still coupled | Multiple services that must deploy together |
| Shared database | Boundary illusion — data not owned | Two components sharing one schema |
| God component | Everything depends on one thing | The bottleneck for every team |
How to know if this landed
You'll know this has landed when someone stops asking "should we do microservices?" and starts asking "where are the natural seams in our domain?" They can identify the axis of change in their system. They draw boundaries along what changes independently, not along technology layers. They catch shared databases and chatty boundaries before they become structural problems. And they can explain bounded contexts using their own business vocabulary — not DDD jargon.
What changes when the mental model clicks
I've run this session with teams ranging from startups with a growing monolith to enterprises with thirty services that deploy together. The gap at the start is usually not about technology — it's about not knowing where to draw the lines.
What changes after this session:
Teams stop decomposing by technology layer and start decomposing by domain. The axis of change test — "what changes together, what changes apart" — is always the moment things click. People stop treating microservices as a goal and start treating them as one possible outcome of good decomposition. Their results get better. They stop blaming the monolith when the real problem was that they drew the boundaries wrong.
The bounded context exercise tends to immediately change how teams talk about their domain. They start noticing that "order" means different things to different people. Their models get cleaner. They stop fighting their own system when the real problem was that they were using one model for multiple contexts.
Book a Workshop
Ready to decompose your systems deliberately?
or
1.5-day workshop includes domain decomposition exercise with your real business domain, boundary design workshop — draw the seams for your system, coupling and cohesion assessment of your current architecture, bounded context identification and ubiquitous language definition, decomposition canvas creation for your system, and anti-pattern identification — find the mistakes in your current decomposition.