The Law of Demeter
Law of Demeter — only talk to your immediate neighbours, never reach through a.getB().getC() chains. Train-wreck chains couple you to the whole object graph's shape; "tell, don't ask" moves behaviour to the data and shrinks the blast radius of structural change.
You write one innocent line: order.getCustomer().getAddress().getCountry().getCode(). It compiles, it works, it ships. Six months later someone splits Address into BillingAddress and ShippingAddress, and that line — along with forty others shaped just like it — breaks. None of those lines used the address; they only walked through it to reach a country code. Yet every one of them had quietly memorised the entire path.
That is the cost of talking to strangers. A method that reaches three or four objects deep doesn’t just depend on its neighbour — it depends on the neighbour’s neighbour’s internal shape, all the way down. The Law of Demeter is the rule that stops you from signing that contract by accident.
After this lesson you can state the Law of Demeter as a concrete “who may I call?” rule rather than a vague slogan; recognise a train-wreck chain and explain exactly which structural changes it couples you to; apply “tell, don’t ask” to move behaviour to where the data lives so the chain disappears; and — critically — know when not to apply it, so you don’t generate an army of pass-through wrapper methods (the Middle Man smell) chasing a metric.
The Law of Demeter is a precise rule, not “avoid dots”. A method m on object O may only call methods on a small, named set of objects: O itself (its own fields and methods), the parameters passed into m, any objects m creates, and O’s direct component objects. It may not call methods on objects returned by those calls. In one sentence: talk to your friends, not to strangers your friends introduce you to.
class ReportService {
constructor(private readonly db: Database) {}
build(request: ReportRequest): Report {
this.normalise(request); // OK — own method
request.validate(); // OK — a parameter
const rows = this.db.query(); // OK — own field
const out = new ReportBuffer(); // OK — created here
return out.finish(); // OK — object we made
// NOT OK: this.db.getConnection().getPool().getStats()
// — getConnection() returns a stranger; we never met the pool.
}
}The rule is about return values. The moment you call a method on something another method handed back, you’ve stepped past your friends and started depending on a stranger’s internals.
A train-wreck chain couples the caller to the entire path’s shape, not just its endpoint. Consider order.getCustomer().getAddress().getCountry().getCode(). This line cares about one thing — a country code — but it has structurally bound itself to four facts: that Order exposes a Customer, that Customer exposes an Address, that Address exposes a Country, and that Country exposes a getCode(). Change any link and the caller breaks.
// The caller "knows" the whole graph just to read one leaf:
const code = order.getCustomer().getAddress().getCountry().getCode();Now multiply by every place that does the walk. The blast radius of “rename Country.getCode() to getIsoCode()” is no longer Country — it is every call site that ever traversed through a country to reach the code. The chain turned a local change into a global one. This is why structural coupling, not behavioural coupling, is the expensive kind: behaviour you can mock, but shape you have hard-wired into the caller.
The fix is “tell, don’t ask”: move the behaviour to where the data lives. Instead of asking an object for its parts so you can compute something, tell the object what you want and let it compute it with parts it already owns. The chain collapses because each object only ever reaches one hop.
class Country { constructor(private code: string) {} isoCode() { return this.code; } }
class Address { constructor(private country: Country) {} countryCode() { return this.country.isoCode(); } }
class Customer { constructor(private address: Address) {} countryCode() { return this.address.countryCode(); } }
class Order { constructor(private customer: Customer) {} countryCode() { return this.customer.countryCode(); } }
// Caller now talks only to its friend:
const code = order.countryCode();The caller depends on exactly one thing: that Order can tell it a country code. How Order gets there — through customer, address, country — is now Order’s private business. Rename Country.getCode, restructure Address, swap the whole chain for a cache: the caller never notices. You have traded a wide, brittle dependency for a narrow, stable one.
The failure mode: dogmatic LoD breeds the Middle Man smell. Look again at Address.countryCode() and Customer.countryCode() — they do nothing but forward the call one level down. Apply the law mechanically across a large graph and you generate a forest of these pass-through methods: every layer grows a delegating method for every leaf any caller might want. Now a new field on Country means adding the same trivial forwarder to Address, Customer, and Order. You’ve replaced one localised change-amplification (the chain) with another (the wrapper cascade). When a class delegates almost everything and adds nothing, that is the Middle Man code smell, and the cure is the opposite refactor — Remove Middle Man: let the caller talk to the delegate directly.
// Middle Man: Order forwards everything to Customer and contributes no logic.
class Order {
constructor(private customer: Customer) {}
name() { return this.customer.name(); }
email() { return this.customer.email(); }
countryCode() { return this.customer.countryCode(); }
tier() { return this.customer.tier(); } // ...and on, and on
}So the Law of Demeter is a heuristic about coupling, not a syntactic ban on chained calls. The senior question is never “are there two dots?” but “does this chain make me depend on structure I have no business knowing — and does hiding it add real behaviour, or just a forwarder?”
A violation that hides a real bug, and the fix that prevents it. A discount routine reaches through the order graph to decide free shipping:
function shippingFee(order: Order): number {
// Train wreck: walks order → customer → membership → plan
if (order.getCustomer().getMembership().getPlan().getName() === "premium") {
return 0;
}
return 5.0;
}Two problems. First, structural coupling: this line knows that customers have memberships, that memberships have plans, and that plans have names — four facts it should never need to compute a fee. Second, a latent crash: a guest checkout has no membership, so getMembership() returns null and the chain throws TypeError. The bug is invisible until a guest hits checkout, because the chain assumes the whole path exists.
Now tell, don’t ask — ask the customer the question you actually have:
class Customer {
constructor(private membership: Membership | null) {}
hasFreeShipping(): boolean {
return this.membership?.isPremium() ?? false;
}
}
class Membership {
constructor(private plan: Plan) {}
isPremium(): boolean { return this.plan.isPremium(); }
}
function shippingFee(order: Order): number {
return order.customer.hasFreeShipping() ? 0 : 5.0;
}The fee routine now depends on one thing — that a customer can answer “do you get free shipping?” — and the null case is handled once, inside Customer, where the knowledge of “a guest has no membership” actually belongs. Restructure plans, add a second premium tier, change the rule entirely: shippingFee never changes. The chain didn’t just couple us; it leaked a precondition (membership ≠ null) into a place that had no business enforcing it. Moving the behaviour to the data fixed the coupling and the bug in one move.
▸Why this works
Why does Demeter target return values specifically, and not just “long lines”? Because a return value is an object you were never introduced to — you obtained it by reaching past your collaborator into its results. Calling your own fields, your parameters, and objects you constructed is fine no matter how many of them there are: those are relationships you declared. The danger is the implicit, undeclared dependency you pick up when you start navigating someone else’s output. That is also why the law is sometimes called the principle of least knowledge: a unit should know about as few other units as possible, and nothing about the units those units happen to hold.
▸Common mistake
The most common overcorrection is treating “no chains, ever” as the goal and counting dots like a linter. Two legitimate exceptions: fluent builders/APIs (query.select(...).where(...).limit(10)) return the same conceptual object configured step by step — there is no stranger, so chaining is fine. And plain data structures — DTOs, JSON config, value objects with no behaviour — exist precisely to be navigated; config.server.tls.minVersion is reading data, not commanding behaviour, and wrapping every leaf in a method buys nothing. Demeter is about not reaching through objects with behaviour to drive their collaborators. Reading a field off a record, or chaining a builder that hands itself back, is not what the law forbids.
A reviewer flags every multi-dot expression in a PR, including config.database.pool.maxSize on a plain config object and queryBuilder.from('t').where(...).limit(5) on a fluent builder, demanding wrapper methods for each. What is the senior assessment?
The Law of Demeter — the principle of least knowledge — says a method may only call methods on its own fields, its parameters, objects it creates, and its direct components, never on objects those calls hand back. The reason is structural coupling: a train-wreck chain like a.getB().getC().getD() binds the caller to every link’s shape, so any restructuring anywhere on the path ripples out into a wide blast radius. The fix is tell, don’t ask — push the behaviour down to where the data lives so each object reaches only its own neighbour, confining structural change to one boundary (and, as the worked example showed, often killing a latent null-chain bug along the way). But the law is a heuristic about coupling, not a syntactic dot-count: applied dogmatically it spawns the Middle Man smell — cascades of forwarding wrappers that just relocate the change-amplification. Fluent builders that return themselves and plain data structures you navigate are legitimate exceptions. Ask not “how many dots?” but “am I depending on structure I have no business knowing?”
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.