skip to content

How do you decide where to draw boundaries when designing a new system or decomposing an existing one?

level: seniorimportance: should knowfreq 50%

answer

  1. capability, not layer
  2. one writer per datum; aggregates stay whole
  3. co-change history = evidence, not opinion
  4. cut where the traffic graph is thin
  5. logical early, physical late

basics

~20 s

Put boundaries where things change for different reasons and rarely change together. Group by business capability and by who owns the data, not by technical layer; check candidate lines against real change history and against how chatty the two sides would be.

solid answer

~50 s

Draw boundaries along axes of independent change, not along technical layers. Practical inputs: (1) business capabilities and the ubiquitous language — where the meaning of a term shifts, a context boundary is nearby; (2) data ownership — each piece of data should have exactly one writer, and aggregates/invariants that must be transactionally consistent belong on one side; (3) change coupling evidence — mine version-control history for files that change together; a candidate line that most commits straddle is misplaced; (4) rate and reason for change — volatile vs. stable, and different regulatory/security domains; (5) interaction volume — put the line where the traffic between the two sides is thinnest, since that becomes the crossing cost; (6) team and Conway's law — boundaries you can't staff will erode. Techniques: event storming and domain storytelling to surface contexts, the Single Responsibility/Common Closure principles for module cohesion, and the Common Reuse principle for release granularity. Validate by prototyping the contract and checking that plausible near-term features land inside one side.

go deeper

for a junior

Say boundaries go around things that belong together and change together — by business area, not by technical layer.

for a middle

Add data ownership, cohesion/coupling, avoiding chatty cuts, and the layer-vs-capability contrast with a concrete example.

for a senior

Bring evidence-based methods (co-change analysis, event storming), aggregate/transaction constraints, Common Closure/Common Reuse, and validation of a candidate contract before committing.

for a principal

Sequence the decision: logical boundaries early and cheap, physical late and evidence-driven; align with team topology and Conway; explicitly preserve the option to move a boundary and state what would falsify the current cut.

## The governing idea A boundary is worth drawing where the two sides **change independently**. Everything below is a way of finding, or falsifying, that hypothesis. ## Heuristics that actually work ### 1. Business capability, not technical layer Decomposing by layer (`controllers` / `services` / `repositories`) gives modules that all change together for every feature: adding a field touches all three. Decomposing by capability (`Ordering`, `Pricing`, `Fulfilment`, `Identity`) gives modules where a feature usually lands in one. Layers are still useful *inside* a capability; they are a bad top-level decomposition. ### 2. Language shift = context edge In DDD, when the same word means different things to different people — 'Policy' in Underwriting vs. Claims, 'Order' in Sales vs. Warehouse — you're at a **bounded context** edge. Techniques to surface these: **event storming** (map domain events on a timeline with domain experts and look for clusters and pivotal events), **domain storytelling**, and simply listening for where experts start disambiguating terms. ### 3. Data ownership and transactional invariants Ask: *which invariants must be enforced atomically?* Data that must be consistent in one transaction (a DDD **aggregate**) cannot be split across a boundary you intend to make remote. Conversely, each piece of data should have **one writer**; if two candidate modules both write the same table, either the boundary is wrong or one must become the owner and expose an operation. ### 4. Change-coupling evidence The most objective input available: mine your VCS history. For each pair of files/packages, compute how often they change in the same commit or PR. High cross-boundary co-change means the boundary is misplaced; high within-boundary co-change confirms cohesion. This is 'behavioural code analysis'. Robert Martin's **Common Closure Principle** states the same rule prescriptively: classes that change for the same reasons and at the same times belong in the same component. The **Single Responsibility Principle** is its class-level analogue — a module should have one reason to change, i.e. one primary actor/stakeholder it answers to. ### 5. Volatility and reason for change Separate the stable from the volatile: a stable domain core from a volatile integration adapter, a stable policy engine from frequently-changing promotional rules. Also separate different *reasons*: different regulators, different security classifications (card data, PII), different customers/tenants. ### 6. Interaction volume — cut where the graph is thin Model the call/data graph and place the line where it has fewest edges (a graph min-cut intuition). A boundary that sits across the hottest path becomes the chatty boundary you'll spend a year optimizing. ### 7. Different non-functional profiles Different scaling curve, latency budget, availability target, or hardware need is a legitimate boundary driver, and one of the few that justifies a *physical* split on its own. ### 8. Organization — Conway's law System structure tends to mirror communication structure. A boundary that cuts across a single team's daily work will erode; a boundary aligned to team ownership tends to hold. The **inverse Conway manoeuvre** shapes teams to produce the desired architecture. Don't design more boundaries than you have owners for. ## Guardrails and anti-patterns - **Entity services** — `CustomerService`, `OrderService` that are just CRUD over a table. They create maximum chattiness (every use case orchestrates several of them) and no behavioural cohesion. Prefer capability/use-case oriented boundaries. - **Boundaries by noun** — 'we have five nouns so we'll have five modules'. Nouns are not change axes. - **Premature decomposition** — you know least about the domain at the start, yet up-front splitting demands you know most. Prefer a coarse start with enforced internal boundaries, then split on evidence. Conversely, note the counter-argument: boundaries you never draw are hard to introduce later in a tangled codebase, so *some* internal modularity from day one is cheap insurance — the point is that **logical** boundaries early, **physical** boundaries late is the low-regret ordering. - **Shared 'common'/'utils' module** — becomes a hidden coupling hub that everything depends on and everyone changes. Split by purpose or duplicate small helpers. Related: the **Common Reuse Principle** — classes that are not reused together should not be grouped together, because consumers are then forced to redeploy for changes they don't care about. - **Distributed transactions as a design goal** — if your boundary forces two-phase commits, it's cutting an aggregate in half. ## Validating a candidate boundary Before committing, test the hypothesis: 1. Write the contract first. If you cannot express it without exposing the other side's internals, the line is wrong. 2. Take the last N features and the next N roadmap items and ask which side each lands on. If most straddle, move the line. 3. Estimate crossing traffic. If the primary read path needs a join across the line, either denormalize/replicate deliberately or merge. 4. Ask who owns each side operationally. No owner, no boundary. 5. Check the dependency direction is one-way and cycle-free. ## Boundaries are revisable Treat the first cut as a hypothesis. Keep logical boundaries movable (in-process, one repo, refactorable) until evidence accumulates; only then make them physical, because that's the point where moving them becomes expensive. The most valuable architectural skill here is not picking the perfect boundary — it is preserving the ability to change your mind.

  • What objective evidence can you use rather than intuition when choosing boundaries?
    Version-control co-change analysis (which files/packages change in the same commits), production call graphs and traffic volumes between candidate sides, incident history showing what fails together, and the distribution of recent feature work across the proposed line.
  • Why are 'entity services' like CustomerService and OrderService usually a poor decomposition?
    They mirror tables, not capabilities, so behaviour lives in orchestrators rather than in the service. Every use case must call several of them, making all boundaries chatty, and no single service can enforce a meaningful invariant on its own.
  • How does Conway's law affect boundary choice?
    The architecture drifts toward the org's communication structure, so boundaries that don't match team ownership erode as people take shortcuts across them. Either align boundaries with teams or deliberately reshape teams (inverse Conway) to sustain the architecture you want.

Deciding where to put walls in an office. You don't wall off 'all chairs' from 'all desks' (that's layering); you wall off teams that work together and rarely need each other. And you watch where people actually walk — a wall across the busiest path just adds doors and queues.

context