What is Law of Demeter (Principle of Least Knowledge)?
You want to send a letter to a colleague. You don't go to HR, then to accounting, then to IT to find their address. You just ask your manager: "Send a letter to colleague X." Th...
🟢 Junior Level
Simple Definition
Law of Demeter (LoD) is a rule that says: “Talk only to your immediate friends.” An object should only know about its direct “friends” and should not “dig into” the internals of objects that were passed to it.
Analogy
You want to send a letter to a colleague. You don’t go to HR, then to accounting, then to IT to find their address. You just ask your manager: “Send a letter to colleague X.” The manager knows how to do it. You shouldn’t need to know the company’s internal structure.
Simple Example
// BAD: violating LoD — "train wreck"
String city = order.getCustomer().getAddress().getCity().toLowerCase();
// GOOD: respecting LoD — delegation
String city = order.getDeliveryCity(); // Order knows where to get the city
When to Use
- When you see chains of 3+ dot calls:
a.getB().getC().getD(). - When a change in one class breaks code in five others.
- When you have to create complex mock trees for tests.
🟡 Middle Level
How It Works Internally
The Law of Demeter formalizes allowed calls. Method f of object A can only call methods of:
| # | Allowed Object | Example |
|---|---|---|
| 1 | The object A itself |
this.someMethod() |
| 2 | Method f parameters |
param.doSomething() |
| 3 | Objects created inside f |
new Helper().process() |
| 4 | Direct fields (components) of A |
this.field.doSomething() |
Forbidden: calling methods of “friends of friends” — a.getB().getC().method().
Practical Application
“Tell, Don’t Ask” Principle:
// BAD (Ask): querying to decide for the object
if (user.getWallet().getBalance() > price) {
user.getWallet().subtract(price);
}
// GOOD (Tell): telling the object what to do
user.purchase(price); // Wallet is hidden inside, logic is encapsulated
Refactoring Train Wreck via Delegation:
// Before — LoD violation
order.getCustomer().getAddress().getCity();
// After — delegation
public class Order {
public String getDeliveryCity() {
return customer.getAddress().getCity(); // Order knows its Customer
}
}
Comparison with Alternatives
| Approach | Pros | Cons |
|---|---|---|
| LoD (delegation) | Encapsulation, easy to change internals, simple mocks in tests | More delegate methods, “proxy noise” |
Train Wreck (chains) (Train Wreck — a long chain of calls a.getB().getC().getD(), fragile and hard to read) |
Less code, faster to write | Fragility: change in Address breaks 10 classes, complex mocks |
| DTOs with public fields | Simple for data transfer | No behavior, anemic model, LoD not applicable |
LoD vs Fluent Interface
// This is NOT an LoD violation — this is Fluent Interface
list.stream().filter(x -> x > 0).map(String::valueOf).collect(toList());
Why? Each method returns an object of the same type (or its transformation) — this is a pipeline, not “traveling through someone’s internals.” The context doesn’t change.
When NOT to Use
- DTO / Data classes — if an object is just a “data bag” without behavior, chains are acceptable.
- Builder pattern —
new User.Builder().name("x").age(25).build()— not an LoD violation, this is a DSL. - Stream API / method chains — pipeline with a single context.
- Test code — in tests, freer chains for asserts are acceptable.
🔴 Senior Level
Deep Internal Implementation
Bytecode Level and Performance
Each .getMethod() call in a chain is an invokevirtual at the bytecode level:
// order.getCustomer().getAddress().getCity()
// Compiles to:
INVOKEVIRTUAL Order.getCustomer()LCustomer;
INVOKEVIRTUAL Customer.getAddress()LAddress;
INVOKEVIRTUAL Address.getCity()LString;
Each invokevirtual:
- Checks the vtable (JVM’s virtual method table that maps a method call to its actual implementation based on the object type).
- May not be inlined by JIT if the type is unknown.
- In a chain of 4 calls — 4 vtable checks, 4 potential misses for inlining.
Delegation reduces the chain to a single call that the JIT (JVM’s Just-In-Time compiler) can fully inline:
// order.getDeliveryCity() — one invokevirtual
// JIT inline: the method body = Order.getCustomer().getAddress().getCity()
// But the JVM sees one call and may inline everything into the caller.
Architectural Trade-offs
| Approach | Pros | Cons |
|---|---|---|
| Strict LoD (delegation) | Maximum encapsulation, stable APIs, simple mocks | Delegating class bloated with “transit” methods that carry no logic |
| Relaxed LoD (2-level chains) | Balance between readability and encapsulation | Still fragile under deep changes |
| Ignoring LoD | Minimal boilerplate | Fragile code, “crystal” change effect, complex mocks |
Edge Cases
1. DTOs and Anemic Model:
// DTO — LoD doesn't apply by definition
orderDto.getCustomer().getAddress().getCity();
DTOs are data structures without behavior. LoD protects behavioral encapsulation, which DTOs don’t have. But if a DTO has 5+ nesting levels — it’s a signal that the domain model isn’t well designed.
2. Null in a Chain:
// Without LoD — NullPointerException at any link
order.getCustomer().getAddress().getCity(); // NPE if customer == null
// With LoD — centralized null-check in one place
public String getDeliveryCity() {
return customer != null && customer.getAddress() != null
? customer.getAddress().getCity()
: null;
}
LoD allows centralizing null-checks in the delegating method.
3. Immutable Objects and Chains:
// Immutable Builder — formally an LoD violation, but acceptable
new User.Builder().name("x").age(25).email("a@b.com").build();
Each call returns a new Builder (or the same mutable one). This isn’t “traveling through someone’s internals” — it’s step-by-step construction of one object. LoD doesn’t apply here.
Performance Implications
| Metric | Train Wreck (4 calls) | Delegation (1 call) |
|---|---|---|
| Bytecode instructions | 3× invokevirtual | 1× invokevirtual |
| JIT inline potential | Low (4 methods) | High (1 method → inline) |
| CPU cache | Miss on each call | One call, cache-friendly |
| Benchmark (ops/ms) | ~120,000 | ~180,000 (+50%) |
In absolute numbers, the difference is nanoseconds per call. But in a hot path with millions of iterations (e.g., JSON serialization), delegation gives a measurable gain.
Memory Implications and GC Impact
- Delegation creates additional methods in the class. Each method is an entry in the constant pool and bytecode array. In practice: 100 delegate methods = ~5-10 KB of additional class file. This is negligible.
- Train Wreck doesn’t create additional methods but requires loading all intermediate classes. If
Addressis used only in one chain — it’s a “dead” class loaded into permgen/metaspace.
Thread Safety
- Train Wreck — each call in the chain can return a mutable object that another thread might modify between calls. Race condition:
order.getCustomer()returned one customer, butcustomer.getAddress()— already a different one (if the reference changed). - Delegation — allows taking an atomic snapshot of state inside a single method with proper synchronization.
public synchronized String getDeliveryCity() { return customer != null ? customer.getAddress().getCity() : null; }
Production War Story
Problem: In a notification service, there was code:
notification.getTemplate().getTheme().getLayout().getHeader().getCssClass()
During refactoring, Layout was renamed to PageLayout, and 23 classes across 5 modules broke. Tests didn’t cover 8 of them — they were in a legacy module without CI. The bug reached production: notifications arrived without styles for 3 hours.
Diagnosis: SonarQube showed 47 train wreck chains longer than 3. An ArchUnit rule noClasses().that().haveNameNotMatching(".*Dto.*").should().accessChainDepth().greaterThan(2) was added.
Solution:
- Introduced delegating methods:
notification.getHeaderCssClass(),notification.getBodyCssClass(). - Separated DTOs from domain objects — DTOs allow chains, domain objects — don’t.
- ArchUnit rule in CI blocks PRs with train wreck chains > 2 links.
Monitoring and Diagnostics
ArchUnit — architectural tests:
@ArchTest
static final ArchRule no_train_wrecks =
noClasses().should().callMethodWhere(
target(owner(assignableTo(Object.class)))
.and(target(assignableTo(Object.class)))
); // Simplified: forbids method calls on call results
SonarQube — rule S2259 (null pointer dereference) often catches train wrecks. The SonarJava plugin has a “Cyclomatic Complexity of Call Chains” metric.
Structure101 / jQAssistant — tools for visualizing connectivity “stars.” Train wreck chains appear as long edges in the dependency graph.
IntelliJ IDEA — built-in “Chain of method calls” inspection (Settings → Editor → Inspections → Java → Code style).
Best Practices for Highload
- Delegation in hot path — one invokevirtual instead of three gives JIT-friendly code.
- Immutable chains — if a chain is unavoidable, use immutable objects (no null possible, race conditions excluded).
- DTO vs Domain — strictly separate: DTOs allow chains, domain objects delegate.
- ArchUnit in CI — automatic train wreck blocker in PRs.
- “Maximum 2 dots” rule —
a.b()OK,a.b().c()— borderline,a.b().c().d()— forbid.
Summary for Senior
- LoD is about encapsulating boundaries. Each
.is a potential failure point. - Train wreck — not just “ugly” — it’s a measurable performance penalty and architectural fragility.
- Delegation — not boilerplate, but an investment in API stability.
- Fluent Interface / Stream API / Builder — not LoD violations, these are a different interaction pattern.
- LoD is closely related to “Tell, Don’t Ask” — both principles protect behavioral encapsulation.
🎯 Interview Cheat Sheet
Must know:
- LoD: “Talk only to your immediate friends” — don’t call
a.getB().getC().getD() - Train Wreck — a long chain of calls, fragile and hard to read
- “Tell, Don’t Ask” principle: tell the object what to do, don’t ask for its data to decide for it
- Allowed calls: own methods, method parameters, created objects, direct fields
- Fluent Interface / Stream API / Builder — NOT LoD violations (pipeline with one context)
- Delegation vs Train Wreck:
order.getDeliveryCity()instead oforder.getCustomer().getAddress().getCity() - “Maximum 2 dots” rule:
a.b()OK,a.b().c()borderline,a.b().c().d()— forbid
Frequent follow-up questions:
- Why is Train Wreck a problem? — 3× invokevirtual, each may not be inlined by JIT, NPE at any link, complex mocks
- Does LoD apply to DTOs? — No, DTOs are structures without behavior; but 5+ nesting levels = signal of a bad domain model
- Is Builder pattern an LoD violation? — No, it’s DSL/step-by-step construction of one object, not “traveling through internals”
- Performance: Delegation vs Train Wreck? — Delegation: 1 call (JIT-friendly), Train Wreck: 3+ calls (+50% ops/ms)
Red flags (DO NOT say):
- “Chains of 5 calls are fine, they’re more readable” (fragility: change in one class breaks 10 others)
- “LoD always requires delegate methods” (DTOs, Builder, Stream — legitimate exceptions)
- “Delegation is boilerplate” (it’s an investment in API stability)
Related topics:
- [[12. What is delegation in OOP]]
- [[10. What is composition and inheritance]]
- [[7. What is Interface Segregation principle]]
- [[22. What anti-patterns contradict SOLID principles]]