Skip to content

Coupling and Cohesion

The previous topic listed what each style costs. It did not say how to tell which one a particular system needs.

That judgement comes down to two properties. They are old ideas, usually taught at the level of classes and functions; what follows is the same pair applied to boundaries between deployables, where the stakes are much higher because the boundaries are expensive to move.

Coupling is how much a change in one part forces a change in another. Cohesion is how much the things inside one boundary belong together.

The design goal is not “low coupling” as an absolute — it is boundaries that contain change. A perfectly decoupled system where every feature touches six services has achieved nothing except distributing the problem.

Put the boundary where change stops, not where the diagram looks tidiest.

Boundaries that contain change — one requirement, one place to edit:

graph LR
  R1["New requirement"] --> A["Part A"]
  A --> OK["Ships from<br/>one deploy"]
  B["Part B"]
  C["Part C"]

  classDef touched fill:#D9695B,stroke:#B4483A,stroke-width:2px,color:#ffffff
  classDef quiet fill:#D8D3CB,stroke:#B8B2A8,stroke-width:2px,color:#3F3F3F
  classDef good fill:#7D9A4E,stroke:#5F7A38,stroke-width:2px,color:#ffffff
  class R1,A touched
  class B,C quiet
  class OK good

Boundaries that do not — the same requirement, four coordinated releases:

graph LR
  R2["New requirement"] --> D["Part A"]
  R2 --> E["Part B"]
  R2 --> F["Part C"]
  D --> SLOW["Four teams,<br/>four releases,<br/>one order"]
  E --> SLOW
  F --> SLOW

  classDef touched fill:#D9695B,stroke:#B4483A,stroke-width:2px,color:#ffffff
  classDef bad fill:#E8A33D,stroke:#C4832A,stroke-width:2px,color:#ffffff
  class R2,D,E,F touched
  class SLOW bad

The same change against two boundary layouts. Terracotta marks what has to be edited — and the second layout is what “we have microservices but releases take three weeks” looks like from above.

The kinds of coupling worth naming at this altitude

Section titled “The kinds of coupling worth naming at this altitude”

You do not need a taxonomy. You need to recognise the three that make boundaries expensive to move later.

Shared data. Two parts reading and writing the same tables is the strongest coupling available and the least visible on any diagram. Nothing in a service boundary means anything if the database is shared, because the schema becomes a public interface that nobody is versioning.

Temporal coupling. One part must be available at the moment another runs. It is the coupling an event-driven style exists to break, and breaking it is also how you acquire eventual consistency as a product problem.

Semantic coupling. Two parts agree on what a field means. This is the one that survives every refactor, because it is not written down anywhere. A field renamed is caught by a compiler; a field reinterpreted is caught by a customer.

This is the load-bearing idea of the topic.

Every seam you introduce converts in-process coupling into an interface — which is a thing to version, a failure to handle, a latency budget to spend, and a conversation between two teams whenever it changes. Decoupling is a transfer, not a deletion.

So the architect’s question is never “how do I decouple this?” but “where do I want the coupling to live, given which changes I expect?” Put the seams across the axes where change is rare and keep the frequently-changing parts on the same side of a boundary.

The cost of getting this wrong in either direction is asymmetric and worth knowing. Too few boundaries gives you a system that is hard to change safely. Too many gives you a system that is hard to change at all, because every change is a negotiation. The second is harder to recover from, which argues for fewer boundaries than instinct suggests — and for choosing them from evidence.

Change-case analysis: the cheap version of this judgement

Section titled “Change-case analysis: the cheap version of this judgement”

You do not have to guess. Take three to five changes you genuinely expect in the next year, and for each one count the boundaries it crosses.

It costs an hour, it needs no tooling, and it routinely overturns a boundary layout that looked obvious on a whiteboard. The output is not a number — it is the observation that two components always appear together, or that one component is touched by every single change, which means the boundary is in the wrong place.

The limitation, stated honestly: it only tests the changes you thought of. It is evidence, not proof, and it is still far better than drawing boxes by domain noun and hoping.

The mirror property, and the common failure is organising by technical layer rather than by reason-to-change: all the controllers together, all the handlers together. That maximises the number of boundaries every feature crosses, which is the exact opposite of the goal above. Group by what changes together. The cost of doing so is some duplicated plumbing inside each boundary, and that is a price worth paying.

Run change-case analysis before you fill in the trade-off table. If a candidate structure makes two of your expected changes cross three boundaries each, that belongs in the table as a scored weakness rather than as a feeling.


→ Boundaries decided, the gross shape is set. Inside it there is a catalog of smaller, deliberate moves. — The quality tactics catalog