Intermediate12 min readObject-Oriented Foundationslive prototype

Coupling & Cohesion

Aim for HIGH cohesion (each module does one thing well) and LOW coupling (modules barely depend on each other) — the two dials behind almost every "good design" judgment.

The idea

What it is

Almost every time a senior engineer calls a design "clean" or "messy," they're really reading two dials. Cohesion asks: does this module do one focused thing, or a grab-bag of unrelated things? Coupling asks: how much does this module need to know about other modules to do its job? The goal is simple to state — high cohesion, low coupling — and most refactoring is just turning those two dials in the right direction.

Think of a well-run kitchen. The pastry station does pastry, the grill does grilled food, the dishwasher washes — each station is cohesive, focused on one kind of work. And they hand off through a simple counter, not by reaching into each other's drawers — that's low coupling. Now imagine one chaotic station that fries, bakes, plates, and takes payment, constantly grabbing tools from everyone else. That's low cohesion and high coupling: slow, fragile, and impossible to change without chaos.

The one sentence to remember

High cohesion = each module does one thing well. Low coupling = modules barely depend on each other. Push both directions at once and the design almost always gets better.

Mechanics

How it works

These are just two simple questions you ask about any piece of code. That's it — keep these two questions in your head and you understand the whole topic:

  • CohesionDoes this one module stick to a single job? (You want yes → high cohesion.)
  • CouplingHow much does this module need to know about other modules? (You want as little as possible → low coupling.)

1. Cohesion: one module, one job

A module has high cohesion when everything inside it works toward the same job. An InvoiceFormatter that only formats invoices is cohesive — every method belongs there.

It has low cohesion when you've stuffed unrelated jobs into one place — like an OrderManager that validates orders, charges cards, and sends emails. Three different jobs in one box. (Engineers call this a God class.) A quick smell test: if you describe what a class does and you have to say "and… and… and…", it's probably doing too much.

Easy way to spot low cohesion

Vague names like Manager, Helper, Util, or Processor are red flags — they usually mean "a pile of unrelated stuff lives here." A good module name tells you the one thing it does.

2. Coupling: how tangled the wires are

Coupling is about the connections between modules. Low coupling means a module only needs to know a tiny, simple thing about its neighbours. High coupling means modules are tangled together and reach deep into each other.

The everyday symptom of high coupling: you change one thing and something unrelated breaks. A one-line fix turns into "oh, now I also have to update these five other files." When code is loosely coupled, a change stays in one place.

Putting them together

Think of a kitchen. Each station does one job — the grill grills, the pastry station bakes (high cohesion). They hand food off over a simple counter instead of rummaging through each other's drawers (low coupling). The result is fast and easy to change. The opposite — one chaotic station doing everything and grabbing everyone else's tools — is the messy code you're trying to avoid.

The #1 trick to lower coupling: use an interface

Here's the single most useful move. If OrderService builds a StripeGateway itself, it's glued to Stripe forever. Instead, have it ask for a generic PaymentGateway and let someone hand it the real one. Now OrderService doesn't know or care whether it's Stripe or PayPal:

typescript
// ❌ tightly coupled — glued to Stripe
class OrderService {
  private gateway = new StripeGateway(); // builds it itself → stuck with Stripe
}

// ✅ loosely coupled — just asks for "some payment gateway"
interface PaymentGateway { charge(cents: number): void; }

class OrderService {
  constructor(private gateway: PaymentGateway) {} // handed in from outside
}
// switching Stripe → PayPal now changes ONE line — never OrderService itself

That's the whole idea behind the fancy name Dependency Inversion: depend on a simple promise (the interface — "something that can charge a card") instead of a specific thing (the Stripe class). Swapping implementations and testing with fakes both become trivial.

One warning: don't over-split

More modules is not automatically better. If you chop one focused class into ten tiny pieces, each piece looks neat on its own — but now they have to call each other constantly, and coupling shoots back up. The goal isn't more boxes; it's drawing boundaries in the right places, so things that change together stay together.

The one line to remember

High cohesion = each module does one thing well. Low coupling = modules barely need each other. Almost every "clean up this code" task is just nudging these two dials in the right direction.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

Eight responsibilities start dumped into one tangled module — low cohesion, lots of cross-cutting dependency lines. Click a function to move it into the module it belongs to (Orders, Billing, or Notifications) and watch the coupling score fall and cohesion rise as related work gets grouped. Hit Auto-refactor to snap to the ideal, or Tangle it to make a mess again.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Move a function to its home module

The board loads tangled: all eight functions sit in one Unsorted pile with dependency lines crossing everywhere. Click chargeCard, then pick Billing as its destination. Watch the log narrate moved chargeCard → Billing: coupling 9→7 and see the coupling bar drop as that edge stops crossing a boundary.

try 02

Watch cohesion rise as you group

Keep moving related functions together — calcTax and formatInvoice into Billing, sendEmail and logEvent into Notifications, the order functions into Orders. The cohesion indicator climbs as each module fills up with related members, while the coupling score keeps falling. Improving both at once is the whole game.

try 03

Auto-refactor vs. Tangle it

Press Auto-refactor to snap every function into its ideal module — coupling bottoms out and cohesion maxes, showing you the target state. Then hit Tangle it to dump everything back into one module and watch both metrics collapse. The contrast makes "high cohesion, low coupling" visible at a glance.

In practice

When to use it — and what trips people up

When to reach for these dials

Use cohesion and coupling as your everyday review lens, not just for big redesigns. Whenever a class name turns vague (Manager, Util, Helper), whenever one method does several unrelated jobs, or whenever a tiny change ripples across many files, the dials are telling you to split for cohesion and introduce an interface for coupling. The payoff is biggest exactly where code changes most — at the seams between subsystems and around third-party integrations.

Don't chase the metrics off a cliff

"More modules" is not the goal — better boundaries is. Splitting a focused class into a dozen anemic fragments that constantly call each other trades good cohesion for awful coupling. Group what changes together; only draw a boundary where there's a real seam.

What it gives you

  • Changes stay local — a focused, loosely coupled module can be modified without rippling across the codebase.
  • Easier to test in isolation — depend on interfaces and you can mock collaborators trivially.
  • Independent evolution and reuse — swap implementations (Stripe → PayPal, MySQL → Postgres) by changing one wiring line.
  • Easier to read and reason about — one module, one job, with thin, explicit lines crossing its boundary.

Common mistakes

  • Over-splitting into anemic fragments — too many tiny classes that do almost nothing but call each other adds coupling and noise.
  • Hidden coupling sneaks back through shared globals, singletons, or event buses that don't show up as obvious dependencies.
  • Premature abstraction — adding interfaces and indirection before you actually need to swap anything just slows you down.
  • Metrics are a guide, not a target — chasing a cohesion/coupling number blindly can produce technically 'clean' but harder-to-follow code.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// ❌ BEFORE: one God class — low cohesion, tightly coupled to concretes
class OrderManager {
  place(order: Order) {
    if (!order.items.length) throw new Error("empty order"); // validation
    const tax = order.subtotal * 0.2;                        // tax
    new StripeGateway().charge(order.subtotal + tax);        // payment (concrete!)
    new MySqlDb().save(order);                               // persistence (concrete!)
    new SmtpMailer().send(order.email, "Thanks!");           // notification (concrete!)
  }
}

// ✅ AFTER: focused classes, each depending on small interfaces
interface PaymentGateway { charge(cents: number): void; }
interface OrderRepo     { save(order: Order): void; }
interface Mailer        { send(to: string, body: string): void; }

class TaxCalculator { taxFor(o: Order) { return o.subtotal * 0.2; } }
class OrderValidator { validate(o: Order) { if (!o.items.length) throw new Error("empty order"); } }

class OrderService {
  constructor(
    private validator: OrderValidator,
    private tax: TaxCalculator,
    private pay: PaymentGateway,   // interface, injected
    private repo: OrderRepo,       // interface, injected
    private mailer: Mailer,        // interface, injected
  ) {}

  place(order: Order) {
    this.validator.validate(order);
    const total = order.subtotal + this.tax.taxFor(order);
    this.pay.charge(total);
    this.repo.save(order);
    this.mailer.send(order.email, "Thanks!");
  }
}
// swapping Stripe → PayPal, or MySQL → Postgres, never touches OrderService.

References & further reading

5 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 05

What do high cohesion and low coupling each describe?

question 02 / 05

A single OrderManager class validates orders, charges cards, saves to the DB, and sends emails. Which problem is this?

question 03 / 05

You change one class and five others break. Which design quality is poor?

question 04 / 05

How does depending on an interface instead of a concrete class (DIP) reduce coupling?

question 05 / 05

Why can splitting a class too aggressively actually hurt the design?

0/5 answered