Tags: web-dev concept

Coupling and Cohesion

Date: 2026-08-17


How much things depend on each other, and how much a thing’s contents belong together. Nearly every architecture argument — microservices, modules, component boundaries — is one of these two in disguise, which makes them the most useful pair of words in the vocabulary.


Coupling measures how much one component depends on another. Cohesion measures how strongly a component’s own parts belong together.

The goal is low coupling and high cohesion. They pull in opposite directions, which is why it’s a judgement rather than a rule.

HIGH COHESION            LOW COHESION
everything in this       a "utils" file
module is about          containing date
ORDERS                   formatting, an API
                         wrapper and a colour
                         converter

LOW COUPLING             HIGH COUPLING
change orders without    changing orders
touching shipping        breaks shipping,
                         email and reporting

Kinds of coupling, worst to best

CONTENT    A reaches into B's internals
           reading private state, monkey-
           patching
           ← worst. B cannot change anything

COMMON     both share mutable global state
           ← a change anywhere affects
             everywhere

CONTROL    A passes a flag telling B WHICH
           branch to take
           render(data, isAdmin, isCompact)
           ← A knows B's internal structure

STAMP      A passes a whole object when B
           needs two fields
           ← B now depends on that shape

DATA       A passes exactly what B needs
           ← the goal for most code

MESSAGE    A emits an event; B may or may
           not listen; neither knows the
           other
           ← loosest. Also the hardest to
             trace

Control coupling is the one to spot in review. A boolean parameter that switches behaviour usually means two functions wearing one name:

BEFORE                   AFTER
render(data, isCompact)  renderFull(data)
                         renderCompact(data)

The signals

HIGH COUPLING SMELLS
  one change requires edits in five files
  you can't test A without instantiating
    B, C and D
  a shared "constants" or "types" file
    everything imports
  changing a database column breaks the
    front end

LOW COHESION SMELLS
  a file named utils, helpers, common,
    or misc
  a module you cannot describe in one
    sentence
  a class where half the methods use one
    field and half use another

The utils file is the canonical low-cohesion artefact, and it grows because nobody has to decide where anything goes.

The tension

You cannot minimise both. Splitting a module to raise cohesion creates a dependency between the halves, which raises coupling. Merging to reduce coupling lowers cohesion.

one giant module     coupling 0 internally
                     cohesion terrible

fifty tiny modules   cohesion excellent
                     coupling everywhere

The useful question is not “how do I minimise both” but “where do I want the seam?” Put boundaries where change is unlikely to cross them — group by what changes together, not by what looks similar.

BY TYPE (common, often wrong)
  /components  /hooks  /services
  → one feature change touches all three

BY FEATURE
  /checkout  /catalogue  /account
  → a feature change stays in one folder

Where it shows up in the vault

  • Microservices versus monolith is entirely this argument. Services lower coupling at the cost of network calls, distributed transactions and operational overhead — a real trade, not a free win
  • Component API design — every prop is a coupling point, which is why the advice is to expose less — Component API Design
  • Design tokens exist to decouple appearance from components — Design Tokens
  • Immutability removes a coupling nobody declared: two modules sharing a mutable object are coupled through it — Immutability
  • Database constraints versus application checks — where the rule lives determines who is coupled to it — The Relational Model

The honest caveat

Decoupling has a cost, and it’s frequently over-applied. An abstraction layer added for a second implementation that never arrives is pure overhead — indirection to read through, tests to maintain, and a boundary that constrains the one implementation you have.

Couple things that genuinely change together. Premature decoupling is the same mistake as premature optimisation, and it’s harder to undo — Abstraction and Leaky Abstractions.