Beginner14 min readDesign Principles & Heuristicslive prototype

Law of Demeter (Principle of Least Knowledge)

Only talk to your immediate friends — not to strangers you reach through them. A method should call methods on itself, its own fields, its parameters, and objects it makes — but not on objects handed back by another call. Each extra dot (`a.getB().getC().doThing()`) reaches deeper into a stranger's guts and couples you to the whole chain.

The idea

What it is

The Law of Demeter (also called the Principle of Least Knowledge) is a simple rule for keeping objects loosely coupled: only talk to your immediate friends. A method should not go digging through one object to reach another object behind it. The less your code knows about the shape of things around it, the less can break when those things change.

Think about paying at a shop. You hand the cashier your card and they take it from there. You do not reach over the counter, open the cashier's drawer, pull out their personal wallet, and make change yourself. You talk to your friend — the cashier — and let them deal with their own stuff. Code that writes order.getCustomer().getWallet().getCard().getNumber() is doing exactly that rude reach-over: digging through the order, into the customer, into their wallet, all the way down to a card number.

The one sentence to remember

Talk to your friends, not to strangers. Ask the object right in front of you to do the work — don't reach through it to grab something deeper and do the work yourself.

Why it's called a 'law'

It isn't a law of physics — it's a guideline, named after the Demeter research project at Northeastern University in the late 1980s, where it was first written down. 'Principle of Least Knowledge' is the clearer name: a method should know as little as possible about the internal structure of anything else.

Mechanics

How it works

The rule, precisely

A method m of an object O may only call methods on a short list of friends. Anything else is reaching through a stranger.

  • `O` itself — its own other methods (this.helper()).
  • `O`'s own fields — objects it holds directly (this.sender.send(...)).
  • The method's parameters — objects handed in to m (order.total()).
  • Objects `m` creates — anything m news up itself (new Logger().log(...)).

What's not on the list is the giveaway: objects returned by another method call. The moment you call a method and then call a method on its result, you're talking to a stranger you were introduced to by a friend. One dot is fine. It's the second dot on a returned object that starts the trouble.

The classic smell: a train wreck

When several of these reaches chain together, you get what people call a train wreck — a long line of dots, each car coupled to the next:

typescript
// ⚠️ a "train wreck" — every dot reaches deeper into a stranger
const number = order.getCustomer().getWallet().getCard().getNumber();

Count what this one line now knows. It knows an Order has a Customer, that a Customer has a Wallet, that a Wallet has a Card, and that a Card has a number. That's four classes welded into a single statement. The day a Wallet stops holding a single Card — say it becomes a list — this line breaks, even though it was only ever trying to charge a card. You reached through three strangers to do it.

Each dot is a piece of knowledge that can betray you

Every .getX() you chain bakes another class's internal structure into your code. Change any link in that chain — rename a getter, swap a type, add a layer — and every train wreck that walked through it breaks. The coupling is invisible until something moves.

The fix: ask your friend to do the work

Don't reach for the data — tell your friend to do the job. Add a method to the object right in front of you (Order) that does the work, so it deals with its own collaborators:

typescript
// ✓ talk to your friend Order; let Order handle its own Wallet & Card
order.chargeCard(amount);

class Order {
  // Order is allowed to talk to its OWN fields — they're its friends.
  chargeCard(amount: number) {
    this.customer.charge(amount);   // pass the job one step down
  }
}

Now your code knows about exactly one class: Order. It has no idea whether a Customer has a Wallet, a Wallet has a Card, or how charging actually happens. Each object handles the layer directly below it and hides the rest. If the wallet-to-card relationship changes tomorrow, only Customer has to know — your code never finds out.

Its cousin: Tell, Don't Ask

The Law of Demeter pairs naturally with Tell, Don't Ask: instead of asking an object for its data and acting on it yourself, tell the object what you want done and let it use its own data. order.chargeCard(amount) (tell) replaces order.getCustomer().getWallet()... (ask). Both push behaviour next to the data it needs.

Where it does NOT apply: this isn't a 'count the dots' law

Demeter is about who you talk to, not about how many dots you type. Some chains are perfectly fine:

  • Fluent builders / chainingnew StringBuilder().append("a").append("b").toString() returns this each time, so every call is still the same friend. No strangers are involved.
  • Streams / pipelineslist.stream().filter(...).map(...).collect(...) chains operations on one pipeline object, not a walk through unrelated classes.
  • Data Transfer Objects — a plain bag of public data with no behaviour (a config tree, a parsed JSON object) isn't a 'stranger' hiding logic; reading nested fields off it is reading data, not violating Demeter.

The test: is each call on the same object, or a deeper one?

Fluent chains call method after method on the same returned object (it returns this). A train wreck calls a method on a different, deeper object each time. Same friend = fine. New stranger each dot = smell.

TRAIN WRECK — reaches through 3 strangers caller Order friend Customer stranger Wallet stranger Card stranger order.getCustomer().getWallet().getCard().getNumber() ×4 REFACTORED — one friendly call caller Order friend order.chargeCard(amount) Order hides Customer, Wallet, Card ×1
Top: the caller's only friend is Order (green), but the train wreck reaches through it to Customer, Wallet, and Card — three strangers (red) introduced by earlier calls. The single line is now coupled to four classes. Bottom: one friendly call, order.chargeCard(amount), leaves the caller coupled to just one class. Order deals with its own Customer/Wallet/Card privately.

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

A live train wreck call chain: order.getCustomer().getWallet().getCard().getNumber() laid out as a row of object boxes (Order → Customer → Wallet → Card) with a caller standing at the left. Click each hop and it lights up as a friend (green — Order is the caller's direct collaborator) or a stranger (red — Customer, Wallet, Card were only reached through other calls). A live counter shows classes this one line is coupled to: 4. Hit Refactor and the chain collapses into a single friendly call order.chargeCard(amount) — the strangers fade away, the counter drops to 1, and the fixed panel narrates you told your friend Order to handle it; Order talks to its own Wallet and Card. One panel, replaced each click; nothing scrolls away.

Hands-on

Try these yourself

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

try 01

Walk the train wreck

Open the prototype. The expression order.getCustomer().getWallet().getCard().getNumber() is drawn as a row of boxes with the caller on the left. Click each hop in turn. Order lights up green — friend, because the caller holds it directly. But Customer, Wallet, and Card light up red — stranger: the caller never knew about them; it was only introduced to each one by the call before it. Watch the counter: classes this line is coupled to: 4.

try 02

Feel why each extra dot hurts

Read the explain panel on each stranger. Reaching Wallet means your one line now knows that a Customer has a Wallet — so if that ever changes, your line breaks even though it only wanted to charge a card. Every red box is a fact about someone else's internals that you've baked into your code.

try 03

Refactor to one friendly call

Hit Refactor. The chain collapses to a single call order.chargeCard(amount), the three strangers fade away, and the counter drops from 4 to 1. Read the panel: you told your friend `Order` to handle it; `Order` talks to its own `Wallet` and `Card` privately. You now talk to one friend and know about one class. That's the Law of Demeter in one move.

In practice

When to use it — and what trips people up

Reach for it when a chain digs through other objects' guts

The Law of Demeter is most useful exactly where a long getter chain is forming. If you catch yourself writing a third dot on a returned object, stop and ask whether the first object could do the whole job for you.

  • Train-wreck getter chainsa.getB().getC().getD(). Push the behaviour onto a so it hides the chain.
  • Code that breaks far from where you edited — if renaming a field in Wallet breaks a checkout screen, a train wreck is reaching through it. Demeter localises that ripple.
  • Rich domain objects — when objects have real behaviour (not just data), 'tell, don't ask' keeps logic next to the data it uses.
  • Wrapping awkward third-party trees — give yourself one friendly method instead of walking a library's nested structure everywhere.

Don't turn it into 'never type two dots'

Demeter is about strangers, not dot count. Fluent builders (builder.a().b().c()), stream pipelines (stream().filter().map().collect()), and plain data objects all chain calls without reaching through strangers — they're fine. Blindly forbidding chains pushes people to wrap everything in pass-through 'delegate' methods, which can bloat your classes more than the chain ever did. Apply it where a chain reveals real coupling, not as a syntax rule.

What it gives you

  • Loose coupling — your code knows about one friend, not a whole chain of classes, so changes deep in the chain don't ripple out to you.
  • Easier to change — when a Wallet's internals change, only its immediate neighbour has to care; distant callers never find out.
  • Information hiding — each object keeps its collaborators private, which is the whole point of encapsulation.
  • Behaviour lands next to data — 'tell, don't ask' nudges logic onto the object that owns the data it needs.

Common mistakes

  • Wrapper / delegate methods multiply — to avoid a chain you often add a pass-through method on each layer (Order.chargeCard → Customer.charge → ...), which is more code.
  • Can be taken too literally — treating it as 'count the dots' flags harmless fluent and stream chains and breeds pointless wrappers.
  • Doesn't fit pure data objects — for DTOs and config trees that are just public data, walking nested fields is reading data, not a violation; forcing methods there adds noise.
  • Hides legitimate structure — sometimes a caller genuinely should know a relationship; over-applying Demeter can obscure a model that was clear.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// BEFORE — a "train wreck": the caller reaches through 3 strangers.
class Card { constructor(private number: string) {} getNumber() { return this.number; } }
class Wallet { constructor(private card: Card) {} getCard() { return this.card; } }
class Customer { constructor(private wallet: Wallet) {} getWallet() { return this.wallet; } }
class OrderV0 { constructor(private customer: Customer) {} getCustomer() { return this.customer; } }

function chargeV0(order: OrderV0, amount: number) {
  // ⚠️ coupled to Order, Customer, Wallet, AND Card — four classes in one line.
  const number = order.getCustomer().getWallet().getCard().getNumber();
  gateway.charge(number, amount);
}

// AFTER — tell your friend Order to do the job; each object handles its own.
class CardA { constructor(private number: string) {} charge(amount: number) { gateway.charge(this.number, amount); } }
class WalletA { constructor(private card: CardA) {} charge(amount: number) { this.card.charge(amount); } }
class CustomerA { constructor(private wallet: WalletA) {} charge(amount: number) { this.wallet.charge(amount); } }
class Order {
  constructor(private customer: CustomerA) {}
  // Order talks only to its OWN field — its friend. The caller knows nothing else.
  chargeCard(amount: number) { this.customer.charge(amount); }
}

function charge(order: Order, amount: number) {
  order.chargeCard(amount);   // ✓ coupled to ONE class: Order.
}

declare const gateway: { charge(num: string, amount: number): void };

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 does the Law of Demeter tell a method to do?

question 02 / 05

Why is order.getCustomer().getWallet().getCard().getNumber() considered a smell?

question 03 / 05

What's the standard fix for a train-wreck chain like order.getCustomer().getWallet().getCard().charge()?

question 04 / 05

Is new StringBuilder().append("a").append("b").toString() a violation of the Law of Demeter?

question 05 / 05

Which principle is the closest cousin of the Law of Demeter?

0/5 answered