practice

Co-Change Analysis

also called Change Coupling, Logical Coupling

Using version-control history to find which components change together - the strongest available evidence that a proposed boundary is right or wrong.

boundariesdecompositionevidenceversion-controlmodularity

Two components that consistently change in the same commits are coupled in the way that matters: not by an import, but by knowledge. A change to the business requires touching both, which means the boundary between them does not correspond to how the domain actually varies.

Co-change analysis extracts that from version-control history. For each pair of modules, how often do they appear in the same change, and what proportion of each one's changes involve the other?

Why it beats other criteria

Every other boundary criterion is a judgement about the future: this will scale independently, this will be owned by a different team, this has a clean interface. Judgements can be wrong, and a wrong service boundary is far more expensive to correct than a wrong module boundary.

Co-change is a measurement of the past. It is not infallible — history may not predict the future — but it is evidence where everything else is opinion.

Implementation patterns

  • Measure over a meaningful period, long enough to include several product cycles. A month of history reflects whatever was being worked on.
  • Exclude mechanical changes — dependency bumps, formatting sweeps, generated code — which create spurious coupling.
  • Look at the asymmetry. If A changes with B in 80% of A's changes but only 10% of B's, A depends on B conceptually and the relationship is directional.
  • Use it to test proposals, not to generate them. "We plan to extract this service" answered with "these two modules co-changed in 60% of commits last year" is a decisive input.
  • Combine with data ownership. A component that co-changes rarely but shares tables is still coupled, just not through code.

Industry example

The recurring failure it prevents is the distributed monolith: services extracted from a design diagram that still require coordinated deployment, so the organisation pays the operational cost of distribution and retains the change cost of a monolith.

This is why the modular-monolith step matters so much. Enforcing module boundaries first, with static dependency checks, and letting change history accumulate produces exactly the evidence needed: modules that have changed independently for a long period are proven extraction candidates, and modules that keep changing together are proof that the proposed boundary is wrong.

Teams that skip the step choose boundaries from a whiteboard and discover them from incidents.

Failure scenarios

  • Analysing too short a window, so the result reflects a single project.
  • Treating high co-change as proof of a bad module, when the modules may legitimately be one concept badly split — the remedy is merging, not more separation.
  • Ignoring data coupling, so components that share tables appear independent.
  • Using it as the only input, ignoring genuine operational reasons to extract a component that happens to co-change.

Trade-offs

The analysis requires history, so it cannot guide the first decomposition of a greenfield system — where judgement is all you have and the mitigation is to start with modules rather than services.

It also measures the past under the current boundaries, which can be self-reinforcing: components that are hard to change separately get changed together, which then argues for keeping them together. Reading it alongside the reasons for each co-change, rather than as a raw statistic, is what avoids that trap.

Interview question

"A team proposes extracting three services from your monolith. What would you look at before agreeing, and what would make you tell them the boundary is wrong?"