At architecture scale, how do you decide where module or service boundaries go using cohesion and coupling — and when is accepting tighter coupling the right call?
answer
- Boundary = bet that change stays inside
- Co-change history beats folder structure
- Conway's Law: boundaries follow teams
- Immediate invariant ⇒ one transactional boundary
- Modular monolith first; extract when proven
basics
~20 sPut boundaries where things change together and are owned by one team; keep what changes together inside one boundary. Accept tighter coupling when the parts share an invariant that must hold immediately, when the domain is still uncertain, or when decoupling would cost more than the change it saves.
solid answer
~60 sDraw boundaries along **axes of change and ownership**, not along technical layers. Practical inputs: business capability / bounded context, co-change data from version-control history (files that always change together belong together), team topology (Conway's Law — the architecture will mirror communication paths anyway), and transactional invariants (data that must be consistent *immediately* should stay inside one boundary; that is exactly the DDD aggregate rule). Then classify each cross-boundary dependency by the *kind* of coupling: synchronous request-response creates runtime/temporal coupling and multiplies availability; shared databases create common coupling; shared libraries create deployment coupling; events reduce afferent coupling but add eventual consistency and choreography-tracing costs. Accept tighter coupling deliberately when: the invariant demands atomicity; the domain is not yet understood (a modular monolith keeps boundaries cheap to move); duplication would be worse than the dependency; or the decoupling machinery (broker, contracts, versioning) costs more than the change it saves. The failure mode is splitting on technical layers or on org-chart accident, producing a distributed monolith: all the operational cost of services with none of the independence.
go deeper
Say boundaries should group things that change together and that modules should talk through narrow interfaces; give one example such as keeping order pricing in one place.
Add concrete criteria — business capability, team ownership, transactional consistency — and name the mechanisms (synchronous call, shared DB, event) and their costs.
Classify cross-boundary coupling by kind and cost, apply the Stable Dependencies Principle and dependency inversion, and describe when tight coupling is right (atomic invariants, uncertain domain, decoupling overhead).
Frame boundaries as economic bets validated by evidence: co-change mining from version control, Conway's Law and team topology, availability math for synchronous chains, modular-monolith-first with proven-boundary extraction, and outcome metrics like percentage of single-boundary changes and independent deployability.
## The question a boundary answers A boundary is a bet: *changes will be more likely to stay inside it than to cross it.* Get it right and features are single-team, single-deploy. Get it wrong and every feature is a cross-team negotiation. Cohesion and coupling are the language for making that bet explicit. ## Inputs for placing boundaries **1. Axis of change / bounded context.** Group by *who requests the change*, not by what the code looks like. Two modules that both parse text but serve accounting and marketing have different stakeholders and belong apart. Domain-Driven Design's bounded context is the same rule applied to language: where a term's meaning shifts ("Customer" in Sales vs Billing), you have a boundary. **2. Co-change evidence.** Version-control history is the cheapest empirical signal available. Files or packages that repeatedly change in the same commit are cohesive in practice, whatever the folder structure says; a package whose files never co-change is probably two packages. Tools that mine temporal coupling from git logs make this measurable rather than aesthetic. **3. Team ownership (Conway's Law).** "Organisations design systems that mirror their communication structure." A boundary that cuts across a team produces constant coordination and will erode; a boundary aligned with a long-lived team survives. The Inverse Conway Manoeuvre is deliberately shaping teams to get the architecture you want. **4. Transactional invariants.** If two pieces of data must be consistent *at the same instant* (an account balance and its ledger lines), splitting them means distributed transactions or compensating sagas. DDD's rule of thumb — one aggregate per transaction, references between aggregates by identity only — is a cohesion/coupling rule in disguise. **5. Volatility and rate of change.** Isolate the parts that change most often behind stable interfaces so their churn does not propagate. This is Parnas's original information-hiding criterion: hide the decisions most likely to change. **6. Non-functional isolation.** Independent scaling, differing availability or compliance requirements, or a need for a different technology stack can justify a boundary even when the domain does not. ## Classifying the cross-boundary dependency Once boundaries exist, the design work is choosing *what kind* of coupling connects them: | Mechanism | Coupling it creates | Cost | |---|---|---| | Synchronous call (request/response) | **Runtime/temporal**: B must be up now | Availability multiplies (0.99 × 0.99…); latency adds; cascading failure without timeouts, retries with jitter, circuit breakers, bulkheads | | Shared database/table | **Common coupling** | No owner of invariants; schema changes are lock-step; the hardest to undo | | Shared library | **Deployment/version coupling** | Upgrades must be coordinated; a shared "common" package drifts into the zone of pain | | Asynchronous event | Weakest (**message coupling**) | Eventual consistency, ordering/duplicate handling, schema evolution of events, harder end-to-end tracing | | Data replication / read model | Value connascence | Staleness, reconciliation, storage cost | Also useful is the **anti-corruption layer**: when you must depend on a model you do not control (a legacy system, a vendor API), put a translating adapter at the boundary so its concepts do not leak into your core — bounded, deliberate coupling at one point instead of diffuse coupling everywhere. And remember the **direction** rule: dependencies should point from volatile toward stable (Stable Dependencies Principle). Where the natural call direction is wrong, invert it with an interface owned by the stable side, or flip a synchronous call into an event the consumer subscribes to. ## When tighter coupling is the right call Decoupling is not free. Deliberately accept more coupling when: - **An invariant requires immediate consistency.** A saga with compensations is often more expensive and more error-prone than keeping the two entities in one transactional boundary. Prefer one aggregate over eventual consistency for genuinely atomic business rules. - **The domain is still being discovered.** Early boundaries are guesses, and a wrong service boundary is far more expensive to move than a wrong package boundary. A **modular monolith** — enforced in-process boundaries, one deployable — keeps decomposition options open. Extract to services when a boundary has proven stable and there is a real scaling/ownership reason. - **Decoupling machinery outweighs the benefit.** Introducing a broker, contract tests, event versioning and a tracing story to remove one dependency between two modules owned by the same team is negative value. Match the mechanism to the coordination cost it actually removes. - **The alternative is worse duplication.** Duplicating a genuine business rule creates connascence of algorithm across copies — usually worse than a dependency on one owned implementation. (Duplicating *incidentally similar* code is fine; "a little copying is better than a little dependency" applies to accidental, not essential, similarity.) - **The dependency is on something genuinely stable.** Coupling to a mature standard, a value type, or a versioned public contract propagates almost no change, so effort spent hiding it behind an interface buys nothing. ## Failure modes to name - **Distributed monolith** — services split on technical layers or arbitrary lines, chatty synchronous calls, shared database, lock-step releases. All the operational cost, none of the autonomy. - **Entity services** — one service per database table, so every use case orchestrates five calls; boundaries follow data shape rather than capability. - **The shared "common" library** — everything anyone might reuse, imported by all: maximum afferent coupling, concrete, volatile — the textbook zone of pain. - **Over-abstraction** — an interface per class, indirection with one implementation. It reduces the *appearance* of coupling on a diagram while raising the cost of reading the code. - **Nanoservices** — splitting past the point where a change fits in one boundary, converting internal calls into network calls and cohesion into latency. ## How to know you got it right Measure outcomes, not diagrams: what fraction of changes touch exactly one boundary; how many teams must coordinate for a typical feature; lead time from commit to production per boundary; whether a boundary can be deployed and rolled back alone; and whether incidents in one boundary stay contained. If most changes cross boundaries, your cohesion is wrong regardless of how clean the dependency graph looks.
- How do you decide between a synchronous call and an event across a boundary?Ask whether the caller needs an answer to complete its own work. If yes, synchronous is honest — then budget for the availability multiplication with timeouts, retries with jitter, circuit breakers and fallbacks. If the downstream work can happen later, an event removes runtime coupling, at the price of eventual consistency, idempotent consumers and event-schema versioning.
- What evidence would convince you a boundary is in the wrong place?Most features requiring changes in two or more boundaries; releases that must be coordinated; a shared database table written by both sides; chatty synchronous traffic between them; and version-control history showing the two co-change constantly. Those are cohesion failures, and the usual fix is to merge and re-split along the observed co-change lines.
- Why start with a modular monolith rather than services?Because early boundaries are hypotheses. Inside one deployable, a wrong boundary is a refactor; across services it is a migration with data movement, contracts and rollout. Enforce the boundaries in-process (module systems, architecture tests, no cross-package internal imports) so extraction later is mechanical.
Boundaries are like walls in a house. You place them where activities differ and where you need doors you can close, not evenly on a grid. Splitting a kitchen down the middle means every meal crosses a wall — the same cost as a service boundary through the middle of a business capability.
saying these in an interview costs you the question
- Splitting along technical layers (UI / logic / data) instead of business capabilities, which guarantees every feature crosses boundaries.
- Assuming network boundaries reduce coupling on their own — they convert compile-time coupling into runtime coupling.
- Keeping a shared database behind separately deployed services and calling the result decoupled.
- Reaching for sagas and eventual consistency where a single transactional boundary would enforce the invariant for free.
- Judging the architecture by the tidiness of the diagram rather than by percentage of single-boundary changes and independent deployability.