skip to content

Cohesion is usually taught at class level. How does the same principle drive boundaries at module, service and team scale, and what happens when those boundaries are drawn on the wrong criterion?

level: principalimportance: nice to knowfreq 26%

answer

  1. Cohesion is scale-free: statements → methods → classes → packages → services
  2. Common Closure Principle = SRP for components; CCP vs CRP tension
  3. Wrong criteria: technical layer, per-table, 'shared-lib' bucket
  4. Distributed monolith = lock-step releases + chatty hops + shared DB
  5. Evidence: co-change history, release coupling, ticket spread, team diffusion

basics

~20 s

The same rule scales up: group things that change together into the same module or service. Grouping by technical layer instead of by business capability spreads every feature across many deployables, so one change needs many coordinated releases.

solid answer

~50 s

Cohesion applies recursively: statements in a method, methods in a class, classes in a package, packages in a service, services in a platform. At the large scale the right criterion is **change cohesion**, not data or technology similarity — Robert Martin's **Common Closure Principle** says classes that change for the same reason and at the same time belong in the same component. DDD's **bounded context** is the same idea driven by language and business capability; Conway's Law adds that the boundary should also match team ownership, otherwise every change needs cross-team coordination. Drawing boundaries on the wrong criterion — by technical layer (all controllers here, all repositories there) or by entity table — produces a *distributed monolith*: one business change touches five deployables, forcing lock-step releases, chatty cross-boundary calls, distributed transactions and shared databases. The empirical test is temporal/logical coupling from version-control history: if two components are always committed together, they are one cohesive unit that has been split incorrectly.

go deeper

for a junior

Say the same idea scales: things that belong together go in the same module, and features shouldn't be scattered across many places.

for a middle

Contrast package-by-layer with package-by-feature and explain why a feature change touching every layer package signals weak cohesion.

for a senior

Bring in the Common Closure Principle, bounded contexts and the distributed-monolith symptoms, plus concrete evidence like co-change and release coupling.

for a principal

Treat boundaries as revisable bets: quantify with co-change and release-coupling data, align with team ownership per Conway's Law, prefer a modular monolith until a seam proves stable, and accept duplication over a wrongly shared model.

## Cohesion is scale-free The same question — *do these things belong together?* — repeats at every level: | Unit | Cohesive when… | |---|---| | Statements in a method | they perform one step of one task | | Methods in a class | they serve one purpose / one reason to change | | Classes in a package/module | they change together for the same reason | | Modules in a service | they belong to one business capability | | Services in a platform | each is independently deployable and owned | What changes with scale is the **cost of getting it wrong**. A badly split class costs you a refactor in an IDE. A badly split *service* costs you a distributed transaction, a network hop on the hot path, cross-team release coordination, and possibly a data migration to fix. ## The named principles at module scale - **Common Closure Principle (CCP)** — "gather into a component classes that change for the same reasons and at the same times." This is literally SRP/cohesion for components, and it is the primary cohesion criterion above class level. - **Common Reuse Principle (CRP)** — "classes that are used together are packaged together; don't force consumers to depend on things they don't use." CCP and CRP *pull against each other* (CCP grows components, CRP shrinks them), together with the Reuse/Release Equivalence Principle; Martin's "tension diagram" says you deliberately favour CCP early (change is frequent, reuse is speculative) and shift toward CRP as a component matures. - **Bounded Context (DDD)** — a boundary within which a domain model and its ubiquitous language are consistent. Two contexts may both say "Customer" and mean different things; forcing one shared model across them is a cohesion failure that produces a model nobody can change safely. - **Conway's Law / Inverse Conway Manoeuvre** — system structure mirrors communication structure. A boundary that does not match team ownership will be crossed constantly by coordination; teams either route around it or the boundary erodes. ## The classic wrong criteria 1. **By technical layer.** Services named `web-api`, `business-logic`, `data-access`. Every feature — "add a discount field" — touches all three, so nothing can ship independently. High *technological* similarity, zero *change* cohesion. 2. **By entity/table.** A service per database table (`customer-service`, `address-service`) fragments a single business capability into chatty pieces that must be updated together, usually with a shared database underneath. 3. **By reuse fantasy.** A "common" or "shared-lib" component that everything depends on becomes a coincidental-cohesion bucket at platform scale: every team changes it, every change forces everyone to re-release, and its release cadence throttles the whole organization. 4. **By org chart accident** rather than capability, freezing a temporary team structure into the architecture. ## The failure mode: the distributed monolith Symptoms: coordinated/lock-step releases across services; a change to one feature requiring PRs in several repos; synchronous call chains several hops deep on a single user request; distributed transactions or sagas introduced to restore invariants that a wrong boundary broke; a shared database schema several services write to; shared DTO libraries versioned in lock-step. You paid all the costs of distribution (network failure modes, partial failure, operational overhead, eventual consistency) and kept all the costs of a monolith (coupled releases). Almost always the cure is to *move the boundary* — merge services back and re-split along capability lines — rather than to add more integration machinery. ## Measuring cohesion at this scale - **Temporal / logical coupling from version control**: how often do commits touch component A and component B together? Persistent co-change across a boundary means the boundary is in the wrong place. - **Cross-boundary call fan-out per user request**: many hops for one business operation implies a capability split across components. - **Release coupling**: number of components that must be deployed together to ship one feature. The single most damning metric. - **Ticket/epic spread**: how many components a typical feature ticket touches. - **Ownership diffusion**: number of teams committing to one component. ## Trade-offs a principal is expected to raise - **Cohesion boundaries are guesses about the future.** You are betting on which things will change together. Where the bet is uncertain, prefer a *modular monolith* — enforce module boundaries in-process (build-level dependency rules, architecture tests) so the boundary is cheap to move — and extract a service only when a boundary has proved stable and there is an independent reason (scaling, isolation, team autonomy, compliance). - **Some duplication beats wrong cohesion.** Two contexts with superficially identical models are often better off with separate models plus an anti-corruption layer than with one shared model that neither can evolve. - **Sequencing.** Boundary changes at this scale are expensive; prioritize with hotspot and co-change data, and do them one seam at a time behind stable interfaces. - **The organizational half.** Redrawing a service boundary without redrawing ownership just relocates the coordination cost. Boundary, code ownership, on-call, and roadmap should coincide.

  • How do you empirically test whether a service boundary is in the right place?
    Mine version-control and delivery data: how often the two components are committed in the same change, how many components must be released together to ship one feature, and how many teams commit to each. Persistent co-change and lock-step releases mean the boundary cuts through a cohesive capability and should be moved or merged.
  • Why can two bounded contexts justifiably keep their own duplicated `Customer` model?
    Because they mean different things by the word — sales cares about leads and pipeline, support about entitlements and tickets. One shared model must satisfy both and can therefore be changed safely by neither. Separate models with an explicit translation (anti-corruption) layer at the boundary trade a little duplication for independent evolution.
  • When is a modular monolith the more honest answer than microservices?
    When your knowledge of which things change together is still weak. In-process modules with enforced boundaries give you the cohesion benefits while keeping refactoring cheap; you extract a service only once a boundary has been stable for a while and there is an independent driver such as scaling, isolation, compliance or team autonomy.

A hospital organized by tool type — everyone who uses scalpels on one floor, everyone who uses X-rays on another — is technologically cohesive and useless: treating one patient requires trips to every floor. Organizing by capability (cardiology, maternity) keeps each patient's journey inside one unit, which is exactly what capability-aligned services do for a feature.

saying these in an interview costs you the question

  • Splitting services by technical layer or per database table and calling it microservices
  • Believing more services automatically means better modularity
  • Creating a shared 'common' library that every service depends on and every team edits
  • Adding sagas and distributed transactions to compensate for a wrong boundary instead of moving the boundary
  • Ignoring team ownership and Conway's Law when drawing boundaries
  • Treating boundaries as permanent rather than as revisable bets on future change

context