The Law of Demeter: talk to friends only
Open in the interactive app: practice, quiz, mock interview →In one minute
- The Law of Demeter (LoD), or "principle of least knowledge", says a method should only call methods on its close collaborators: its own object, its parameters, objects it creates, and its object's fields. It should not reach through them to objects they return.
order.customer().address().city()breaks it. The calling code now depends on the structure of three classes it does not own, and any change to that structure ripples out to every such chain.- The fix is usually to ask a neighbour to do the work (
order.shipping_city(), or better,order.ship_with(courier)), or to pass in the one thing the method actually needs. - It is not "count the dots". Fluent builders, stream pipelines and operations on plain values are fine, because each call returns the same object or a value, not a stranger's internals.
- Long chains also cost at runtime: dependent pointer loads, a null check per hop, and in ORMs or service calls, one query or network round trip per hop.
How it works
The rule, precisely
The rule comes from the Demeter project at Northeastern University in 1987. Inside a method m of object o, you may call methods on:
oitself.- Parameters of
m. - Objects created inside
m. o's direct fields (its components).
Objects that those calls return are strangers. You may hold them and pass them on, but calling methods on them means you know how your neighbour is built.
The train wreck
A shipping label service needs a city:
def label(order):
city = order.customer().address().city()
if order.customer().account().tier() == "gold":
return Label(city, priority=True)
return Label(city, priority=False)label now knows that orders have customers, customers have one address and an account, accounts have tiers, and that shipping goes to the customer's address. That last one is wrong as soon as a customer can ship to a different address per order, and the fix has to touch every chain like this in the codebase. In the worked example below, the team found 37 of them.
Three fixes
Tell, don't ask. Move the decision to the object that has the data: order.make_label(). The order knows where it ships and how urgent it is.
Delegate one level. If callers truly need the value, add order.shipping_city(). The order can answer from its own shipping address, its customer's default, or a cached field, and callers do not care.
Pass the narrow thing. If label only needs a city and a priority, make those the parameters: label(city, priority). The caller that already has the order extracts them once, and label becomes trivial to test.
Where the law does not apply
- Fluent interfaces.
query.where(...).order_by(...).limit(10)returns the same builder each time. You are talking to one object. - Values and data structures.
name.strip().lower()operates on strings. A parsed JSON config or a DTO has no behaviour to hide; walking it is reading data, not reaching into an object's internals. - Collections and pipelines.
items.filter(...).map(...)is the same.
Over-applying the law produces a different smell: classes full of one-line forwarders like getCustomerAddressCityName(). If an object grows ten of these, the callers probably want a behaviour on that object, not more getters.
Mocks expose violations
A test for the train-wreck label needs a mock order that returns a mock customer that returns a mock address that returns a city. Three levels of stubs to test one line of logic is a reliable sign that the code knows too much. With label(city, priority), the test needs no mocks at all.
Reviewing agent-written code
Agents produce train wrecks because they navigate whatever object graph they can see in context: ctx.request.user.org.settings.billing.currency. The code works, so tests pass. In review, look for chains through domain objects, especially those that appear in several files, and ask for a method on the nearest owner or a narrower parameter. Telling an agent "functions take the values they use, not the objects that contain them" prevents most of it.
The same shape elsewhere
LoD is the coupling problem that networks solved with layering. An IP host does not know the routers between it and its destination; it hands packets to its default gateway, and each router knows only its next hop. Routes change constantly without any host noticing. Service architectures follow the same rule: a client that calls the order service, then uses an ID from the response to call the customer service, then the address service, is a train wreck across the network.
Down the stack
Layer 1The contract
A method that follows LoD depends only on the interfaces of its immediate collaborators. The testable promise: if a class changes its internal structure (its fields and the types of objects it holds) without changing its own public methods, no method outside it that obeys LoD needs to change. The original paper distinguishes a strict form, which counts only fields declared in the class itself, and a weaker form, which also allows inherited fields.
Layer 2The mechanism
Languages offer tools to obey the law without writing every forwarder by hand. Rails' delegate :city, to: :address generates forwarding methods. Kotlin's by keyword delegates a whole interface to a field. Go struct embedding promotes the inner type's methods onto the outer type, which shortens chains but also publishes the inner API, so embedding a type is a public commitment to its whole method set.
Static analysis can flag violations. PMD for Java has a LawOfDemeter rule that tracks whether each receiver is a field, parameter, local created in the method, or a returned value. It is noisy on fluent APIs, which is why it is rarely enabled by default.
ORMs turn chains into queries. With lazy loading in Django or ActiveRecord, order.customer.address.city issues a SELECT for the customer and another for the address, the first time each is touched. Inside a loop, that is the N+1 query problem. select_related in Django and includes in ActiveRecord fix it with a join, but only if the code that knows what will be read is the code that loads it, which is exactly what a method like Order.shipping_city() centralises.
Null propagation multiplies too. Each hop is a place where the value can be missing. Optional chaining (order?.customer?.address?.city in TypeScript or Kotlin) stops the crash, but it hides which link was missing and silently turns "no address" into undefined.
Layer 3The machine
Each hop in a.b.c.d is a load whose address depends on the previous load. The CPU cannot start loading c until b arrives, so out-of-order execution cannot overlap them. An L1 hit costs about 1 ns, but an object graph that is spread across the heap can miss to main memory at roughly 80 to 100 ns per hop. Three hops over cold objects can cost several hundred nanoseconds per call; in a loop over a million orders, that is a visible fraction of a second.
LoD does not fix pointer chasing by itself. A delegating method in Go or on the JVM is inlined, so order.ShippingCity() compiles to the same loads as the chain. In CPython there is no inlining, and each extra Python-level call adds very roughly 50 to 100 ns. What the law does enable is changing the representation: once only Order knows where the city lives, it can cache it in its own field and turn three dependent loads into one.
Across processes, every hop becomes a round trip. A client that calls three services in sequence pays three network latencies, about 0.5 to 1 ms each inside one data centre, and depends on three schemas deployed by three teams. A protobuf response read as resp.order.customer.address.city couples the client to the nesting of messages it does not own; moving address breaks it on the next release.
Why it's built this way
- Internal structure changes more often than public behaviour, because representations get optimised and requirements reshape relationships.
- Every caller that knows a structure must change when it changes, so the cost of change grows with how far that knowledge spreads.
- Limiting each method to its immediate neighbours bounds that spread to one level.
- Wrapping every path costs boilerplate, so the law is applied to objects with behaviour and relaxed for values, builders and data.
Diagram
flowchart LR L["label()"] -->|"allowed"| O["Order (parameter)"] L -.->|"violation"| C["Customer"] L -.->|"violation"| A["Address"] O -->|"field"| C C -->|"field"| A O2["Order.shipping_city()"] --> SA["shipping address field"] L -->|"after fix"| O2
Worked example
An order history page shows 50 orders, each with its shipping city. The template does order.customer.address.city. With lazy loading, that is 1 query for orders, 50 for customers and 50 for addresses: 101 queries. At about 1 ms per round trip to the database, the page spends roughly 100 ms on queries before rendering.
The team adds Order.shipping_city() and a repository method recent_with_shipping(user_id, limit) that joins orders to addresses in one query. The page drops to 1 query at about 3 ms. The template now calls one method on its own parameter.
Six months later, orders gain a per-order shipping address, separate from the customer's default. The change touches Order.shipping_city() and the repository query. Before the refactor, a search found 37 chains through customer.address in templates, emails, the label service and two reports. Each would have kept printing the customer's default address, silently wrong, after the schema change.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Chain through objects | No extra code | Callers coupled to structure; N+1 queries; null at every hop | Plain data, DTOs, config |
| Tell, don't ask | Behaviour lives with data; callers stay small | Owner class grows | The caller is making a decision the owner could make |
| Delegate method | One place knows the path | Forwarders can pile up | A few values many callers need |
| Pass the narrow value | Simplest to test; no mocks | Caller does the extraction | Functions that use one or two fields |
| Read model or view | Shaped for the consumer, one query | Another model to keep in sync | Pages and reports that read across many objects |
Pitfalls in production
- N+1 queries hidden behind attribute chains in templates. Fix: expose a method that loads what it needs with one query, and set a query-count assertion in tests for list pages.
- Optional chaining that turns missing data into
undefinedand then into "undefined" on a shipping label. Fix: make the owner return an explicit result or error when a link is missing. - Wrapper explosion: dozens of
getXofYofZ()forwarders. Fix: replace them with behaviours the callers actually need, or pass narrow values. - Treating builders, streams and data as violations, which makes code worse. Fix: apply the law to objects with behaviour and invariants.
- Client code that walks nested API responses across service boundaries. Fix: have the owning service return the field the client needs, or give the client its own read model.
- Tests with mocks returning mocks. Fix: change the code under test to take what it uses; the mocks disappear.
Leadership lens
- Count chains through a core domain object before a schema change. The number of call sites that know its structure is the real size of the migration.
- In design review, ask "what does this function actually need?" Narrow parameters make code easier to test, review and reuse, including for agents.
- Treat cross-service chains as an architecture smell: each hop adds latency, a failure point and a team that can break you.
- Do not enforce the law with a linter on every chain. Teams then wrap builders and data access in forwarders, and the codebase gets bigger without getting less coupled.
Check yourself
Why is
items.filter(...).map(...).sum()not a Law of Demeter violation?Show answer
Each step works on a collection or a value that the caller already holds, not on a stranger's internal parts; no domain object's structure is exposed.
A delegating method in Go compiles to the same loads as the chain it replaced. So what did the refactor buy?
Show answer
Only one class now knows the path, so the representation can change in one place, for example to cache the value or load it with a join.
How do long chains through ORM objects cause performance problems?
Show answer
Each hop through a lazily loaded relation can issue a query, so a chain inside a loop becomes N+1 queries.
Why does a test needing mocks that return mocks suggest a design problem?
Show answer
The code under test is navigating its collaborators' structure, so the test must rebuild that structure; taking the needed values directly removes both the coupling and the mocks.
Watch
- Clean Code: The Law of Demeter (Harmony in Code): explains the rule with code, including its downsides and a practical middle ground.
- Clean Code: Explained, Episode 14: Law of Demeter (CodeBuster): a short walkthrough of the rule and how to refactor chains.
Free resources
- Object-Oriented Programming: An Objective Sense of Style (Lieberherr, Holland, Riel, OOPSLA 1988): the original paper with the formal definitions and the strict and weak forms.
- Law of Demeter (Karl Lieberherr, Northeastern): the Demeter project's own page on the law, with history and examples.
- General formulation of the Law of Demeter (Northeastern): the short, language-independent statement of the rule.
- TellDontAsk (Martin Fowler): the companion idea of moving behaviour to the data, and its limits.
- Optional chaining (MDN): how
?.short-circuits a chain, useful to understand what it hides. - Law of Demeter (Wikipedia): background, the formal rule and common criticisms.