Skip to content

5. Coupling and the Principle of Least Knowledge

Chapter 4 gave cohesion its formal home — what belongs inside a unit. This chapter gives coupling its home — what happens between units. They are Chapter 2’s two master metrics, and they pull in tension: split a god function into cohesive pieces and you have just created relationships between those pieces. Coupling is the discipline of keeping those relationships thin. The whole chapter rests on one idea: know as little as the job allows.

Marketing wants VIP customers to get free shipping over $100. The membership data lives a few hops away — an order has a customer, who has a membership, which has a tier — so you write the obvious check:

def free_shipping(order: Order) -> bool:
return order.customer.membership.tier == "gold" and order.subtotal >= 100

It works, ships, and looks harmless. Then, a quarter later, membership gets remodeled: tiers move under a new Plan object, or tier becomes an enum. This line breaks — and it is not alone. The same order.customer.membership.tier reach was copied into the receipt, the analytics export, and three other features, because it was the obvious thing to write each time. One small change to a distant class, and edits ripple across the codebase.

You have seen this exact shape before. It is Chapter 2’s change amplification, and now we can name its cause. That one line knows too much: to compute free shipping it has learned the entire path from order to tier — the existence and shape of three classes it has no real business knowing. Knowledge is coupling, and this line is coupled to everything it touched on the way to tier.

Coupling is how much one unit must know about another to do its job. The Principle of Least Knowledge is the goal: each unit should know as little about the others as the task allows. Coupling is never zero — units that collaborate must share something — so the discipline is not eliminating it but keeping it at the loosest rung that still works.

A practical ladder, loosest to tightest:

  • Data coupling — you pass exactly the value needed: tax_for(amount, country). The callee knows a float and a string, nothing more. This is the rung to aim for.
  • Stamp coupling — you pass a whole object when one field would do. Now the callee depends on that object’s shape, and changes to the shape can reach it.
  • Content coupling — you reach into another unit’s internals: order.customer.membership.tier. The tightest, most brittle rung — a change anywhere along the chain can break you. This is the itch above.

Two techniques keep you near the top of the ladder. The Law of Demeter — “don’t talk to strangers” — says a unit should talk only to its immediate friends, not reach through them to friends-of-friends. The tell-tale is a chain of dots through distinct objects, often called a train wreck: a.b().c().d(). And tell, don’t ask says: don’t pull an object’s data out to make a decision about that object — tell the object, and let it decide. Asking couples you to the data; telling couples you only to a question.

The VIP feature shows both techniques in one move. Instead of reaching through the graph and deciding for the order, we let each object answer for what it owns.

The caller knows the whole path — order → customer → membership → tier — and pulls the subtotal out to decide for the order:

# the caller knows the whole path: order -> customer -> membership -> tier
def free_shipping(order: Order) -> bool:
return order.customer.membership.tier == "gold" and order.subtotal >= 100

Each object answers for what it owns, talking only to an immediate friend, and the caller asks one object one question:

# Membership answers about tiers; Customer about status; Order about its offer.
class Membership:
def is_gold(self) -> bool:
return self.tier == "gold"
class Customer:
def is_vip(self) -> bool:
return self.membership.is_gold() # one hop, to a friend
class Order:
def qualifies_for_free_shipping(self) -> bool:
return self.customer.is_vip() and self.subtotal >= 100
# the caller now asks one object one question:
def free_shipping(order: Order) -> bool:
return order.qualifies_for_free_shipping()

No object reaches past an immediate friend, and the decision lives with the data it concerns. The payoff is concrete: the knowledge of how membership is shaped now exists in exactly one place — Membership — instead of scattered across every caller. When tiers get remodeled next quarter, is_gold (TS: isGold) changes and nothing else does. The examples/ch05/py/ and examples/ch05/ts/ tests prove it the hard way: the after’s free_shipping / freeShipping contains neither the word membership nor tier, so the graph can be rebuilt beneath it without the caller ever knowing.

Coupling has not just a tightness but a direction — and direction is where codebase-scale design begins. When module A imports module B, A depends on B: a change to B can break A, but not the reverse. Read every arrow in this section the same way — A --> B means “A depends on B” — so the question for each dependency is which way the arrow should point.

The rule: depend toward stability. Code that changes rarely (your core domain rules) should not depend on code that changes often (I/O, frameworks, formats). In arrow terms: the stable core should have arrows pointing at it, never out of it.

Here is the wrong way. Someone wants pricing to log every price it computes, so they reach for persistence and import it into pricing:

flowchart TD
    pricing["pricing<br/>(stable core)"] -->|imports| persistence["persistence<br/>(volatile I/O)"]

The arrow points out of the core, toward a volatile detail. Now change the storage format — swap JSON for a database — and pricing can break, though the money math never moved. And pricing can no longer be tested without the persistence layer present. A stable thing has been made hostage to a volatile one. That is backwards.

Here is the right way — the same modules, every arrow reversed onto the core:

flowchart TD
    checkout["checkout<br/>(workflow)"] --> pricing["pricing<br/>(stable core)"]
    checkout --> notify["notify<br/>(volatile I/O)"]
    checkout --> persistence["persistence<br/>(volatile I/O)"]

Every arrow points toward pricing, and none point out of it — which is exactly why Chapter 4 could test it in total isolation. Note what is deliberately not a violation here: the checkout workflow depending on the concrete notify and persistence. Something has to call I/O, and orchestration code depending on details is fine at this scale. Making even checkout depend on abstractions so those details become swappable is the dependency inversion of Chapter 8, and its codebase-scale form is the tiers of Chapter 17. For now, hold the simpler heuristic: arrows point toward the stable, the core points outward at nothing, and no arrow ever forms a cycle.

Python offers no resistance to a train wreck — a.b.c.d is one fluent expression, and nothing at compile time objects. Least Knowledge in Python is therefore a discipline, not something the language enforces. Two features make the disciplined version pleasant. Methods make tell-don’t-ask natural, and @property makes it nearly free: order.is_vip can be a computed question that reads like an attribute, so callers ask without paying for a method call’s ceremony.

The honest caveat: not every dot is a Demeter violation. order.customer.name to print a name is fine. Fluent interfaces that return selfquery.filter(...).order_by(...) — are dots by design, not strangers. A dataclass reaching its own fields is not talking to anyone. The Law of Demeter is about coupling to structure that will hurt you when it changes — a chain across independently-evolving objects — not a literal ban on the dot operator. Count the owners in the chain, not the dots.

Agents reach through object graphs as readily as people do — a train wreck is often the most direct way to get a value, and “direct” reads as “efficient.” They are also prone to the over-corrections above when you ask for a fix without bounding it.

You sayThe agent hears
”Reduce the coupling here”Minimize what each unit must know about the others (Least Knowledge)
“This is a train wreck — fix the Law of Demeter violation”Stop reaching through objects; talk only to immediate friends
”Tell, don’t ask”Move the decision onto the object that owns the data; stop extracting fields to decide
”Pass the value, not the whole object”Drop from stamp coupling to data coupling — depend on a float, not a shape
”Which way should this dependency point?”Depend toward the stable; flag arrows from core to volatile
💬 Find and fix train wrecks (bounded)

Find Law of Demeter violations in this module — chains that reach through more than one object (a.b.c.d). Fix each by adding a method on the object that owns the data, so callers ask one question. Do not create pass-through methods at every level, and if a chain is deep, tell me whether the object graph itself should be flattened instead of wrapped.

💬 Ask-then-decide → tell-don't-ask

This code pulls fields out of an object and then decides something about that object. Refactor to tell-don’t-ask: put the decision on the object as a method or property, and have the caller ask it. Only do this where the decision is about that object’s own state — don’t move behavior that belongs to a different concern onto it.

  • No chain reaching through more than one owner (a.b.c.d)
  • Decisions about an object’s state live on that object
  • Callers depend on small values or single questions, not object shapes
  • Dependencies point toward the stable; the core imports nothing volatile
  • No pass-through wrapper added where a direct access was honest
  • The wrapper forest. Asked to fix Demeter, the agent adds a delegating method at every level of a deep graph — trading a train wreck for a stack of pass-throughs. Bound it: fix the caller; flatten the graph if it’s deep; don’t wrap every hop.
  • The god object. Asked for tell-don’t-ask, it piles unrelated behavior onto the data class until cohesion (Chapter 4) is gone. Ownership is the test.
  • Premature inversion. Hearing “dependency,” the agent introduces an interface and inverts a dependency nobody needed inverted — Chapter 9’s eager abstraction, pointed at coupling. Direction is worth fixing; inversion needs a reason.
  • Coupling is how much one unit must know about another; the Principle of Least Knowledge says know as little as the job allows. It is Chapter 2’s second master metric, and the price you pay for the cohesion of Chapter 4.
  • Aim down the ladder: prefer data coupling (pass the value) over stamp coupling (pass the object) over content coupling (reach into internals — the train wreck).
  • The Law of Demeter (talk only to immediate friends) and tell, don’t ask (decide on the object that owns the data) are both ways to know less. The payoff: knowledge of a structure lives in one place, so changing it touches one place.
  • Coupling has a direction — depend toward the stable, never core-on-volatile, never a cycle. Inversion is Chapter 8; tiers are Chapter 17.
  • Right-size both techniques: Demeter can breed pass-through forests, and tell-don’t-ask can breed god objects (Chapter 4, undone). Ownership is the referee.
  • Glossary terms added: coupling · Law of Demeter (Principle of Least Knowledge) · tell, don’t ask · dependency direction.