
Related content to this article.
Software Architect and Engineer | Java & AI
If you’ve ever stumbled into code like a.getB().getC().doSomething() and felt that “this is going to blow up”, you’re not being dramatic — in practice, it’s an internal detail leaking to whoever is using it.
A module should not know about the innards of the objects it manipulates.
The Law of Demeter (aka the Principle of Least Knowledge) proposes a direct rule: interact only with your “immediate friends.” When you talk to a friend of a friend, you increase coupling and make the code more fragile to internal changes.
The more you depend on an internal path like A → B → C → D, the more fragile the system becomes: change something in the middle and you break places you didn’t even remember existed.
This idea became clearer to me as I kept reading Clean Code (Robert C. Martin), especially Chapter Objects and Data Structures — the Law of Demeter part and the Train Wrecks discussion. That’s where you see why this “navigation style” spreads too much knowledge and turns into technical debt with interest.
Quick connection: Object Calisthenics
In Object Calisthenics, rule #5 is “One Dot Per Line”.
But the point isn’t counting dots — it’s avoiding tourism inside the object (Order → Customer → Address → ZIP code).
The Law of Demeter says: talk only to your “immediate friends” — an object shouldn’t “navigate” inside other objects to reach what it wants.
In practice, it prevents traps like:
The Law of Demeter is not “forbidden to use two dots in one line.” That literal reading is how people mess this up.
Demeter is about knowledge boundaries: a method shouldn’t depend on another object’s internal structure.
Translation: if, to do something, you need to “tour” inside objects (Order → Customer → Address → ZIP code), you’re buying coupling without noticing.
Think of Demeter as a design question: “Should this method even know that this path exists?”
If you want a fast way to decide “friend” vs “friend of a friend,” use this table:
| Situation | Is it a friend? | Example | Why |
|---|---|---|---|
this | ✅ | this.validate() | it’s you |
| Direct field | ✅ | this.customer | you control it |
| Parameter received | ✅ | calculate(Order o) | it came in the signature |
| Object created in the method | ✅ | new Money(...) | you created it, you own it |
| Injected dependency | ✅ | gateway.quote() | explicit collaborator |
Immediate friend = this, direct fields, parameters, objects created in the method, injected dependencies.
Even “pretty” code can hide path dependency.
Chains of calls like this are generally considered to be sloppy style and should be avoided.
The problem isn’t the dot. The problem is that the service becomes dependent on the path.
It works today. Tomorrow someone changes Address, renames something, reshapes the structure… and you break code far away.
Think of a hotel: to get a towel, you talk to the front desk — not the linen room, not the corridor, not the key guy. If the hotel rearranges everything internally, your request still works because your point of contact didn’t change.
In code it’s the same: when you do Order → Customer → Address → ZIP code, you’re not using Order as a public contract — you’re depending on its internal layout.
If I change Address, how many files do I break?
If the answer is “a lot” or “I don’t know,” coupling is already spread out.
You think you fixed it because you split it into three lines? Sorry — you didn’t. You just spread the coupling over three lines. The snippet still depends on the whole path, and address is still a “friend of a friend.”
Syntactically clean. In reality? Same dependency chain. The delivery service still knows the whole route to the ZIP code.
When you apply Demeter, it’s common to think:
“Alright, I’ll create a method on Order and done — no more train wreck.”
This does not break Demeter. Customer is an immediate friend of Order.
The risk is different: if you do this for everything, your class becomes a secretary — full of shortcuts that only forward calls.
You start stuffing Order with “navigation shortcuts”:
The API grows, maintenance gets worse, and you just moved the chain somewhere else.
The same “delegation” is welcome when it carries domain intent and protects the rest of the system from internal structure:
Practical rule (no philosophy):
If the method answers a question that makes sense in the domain (“delivery ZIP code”, “order total”, “can cancel?”), it’s intent.
If it exists only to avoid getA().getB().getC(), it’s a secretary.
When you see a train wreck, two other smells often show up right next to it.
If a method keeps digging into another object’s data to decide something, it’s “living in the wrong place.” It doesn’t use the object — it exploits it.
The problem isn’t the if. It’s the service knowing too much about the internal structure of Customer/Address.
Instead of asking for data and deciding outside, push the decision into the domain (intent method).
If ctxt is an object, we should be telling it to do something; we should not be asking it about its internals.
Code review rule: if you pull data to decide outside, that probably should become an intent method in the domain.
Whether this is a violation of the Law of Demeter depends on whether or not these are objects or data structures.
Demeter matters most where there is behavior and invariants. In pure data, you can be more relaxed. Here are cases where navigating is usually acceptable:
If Address is a real VO (immutable, small, stable, and with no hidden rule), this can be fine:
The idea isn’t “anything goes.” It’s recognizing that reading transparent data is different from breaking domain invariants.
Chaining in stable, well-defined APIs is usually not the problem:
Here the chain isn’t exposing domain internals; it’s just using a consolidated public API.
If chaining exists because the API was designed for it, great:
Besides builders, this also applies to query DSLs and pipelines that return the same type. The point is: this isn’t “path dependency.” It’s an intentional fluent public contract.
Whenever you see a chain, ask:
Address, how many files break?Let’s use a realistic case: shipping calculation. For that, we need the delivery ZIP code (postal code).
The idea is simple: first you’ll see the model, then the train wreck showing up naturally, and finally the refactor that cuts coupling.
To explain Demeter without noise, I used a domain cut that shows up all the time: Order → Customer → Address.
This is common in e-commerce, delivery, ERP, and any shipping/billing workflow.
The chain we want to avoid:
Order → Customer → Address → ZIP code
Address — the destination (where the data we want lives)Address is “the end of the line”: it stores the ZIP code.
When that detail changes (field name, address structure, validation), you don’t want to break half your system because of chained calls.
Customer — the middle link (where coupling starts to grow)Customer represents the “intermediate step” often exposed via getter (getAddress()), and that’s where navigation becomes tempting.
Order — the entry point (the object everyone has in hand)Shipping/checkout rules usually start from an Order.
So when someone needs the ZIP code, the “easy way” becomes: enter the customer, enter the address… until you find the data.
In ShippingService, you just want the ZIP code… but you end up knowing the entire path:
ShippingService knows Order — fine.
What’s not natural is it knowing that Order has Customer, which has Address, which has ZIP code.
Instead of the service navigating through the internal structure of the order, it asks for what it really needs — an intent call, not the path.
The point is: the service only talks to Order.
And here’s the important part: Order exposes an intent method without becoming a “chain warehouse” — it delegates to whoever makes sense.
Now, so this doesn’t turn into “just hiding the chain” inside Order, delegation continues correctly:
Order talks to Customer, and Customer talks to Address.
Checkpoint: the Law of Demeter reduces coupling because the caller stops depending on the internal structure of objects.
If you’ve ever tried to write a simple unit test and ended up with a mock tree… that’s almost never “because testing is annoying.”
It’s because your code is touring inside objects.
If the service fetches the ZIP code via order.getCustomer().getAddress().getZipCode(), the test inherits the same path.
Result: you have to mock Order → Customer → Address just to reach the data.
This test suffers from: coupling to the path, refactors breaking everything, and focusing on structure (not intent).
Catchphrase applied to tests
If I change Address, how many tests break?
If the answer is “a lot,” the problem isn’t the test: it’s the coupling.
In the Demeter-friendly version (ShippingServiceBetter), the service only talks to Order:
So the path is hidden behind an intent method.
And the test becomes simpler:
You test the intent (“delivery ZIP code”), not the structure.
Address) breaks “business rule” tests, your tests are coupled to the access path, not behavior.Demeter isn’t about “how many dots are on the line.” It’s about not depending on the internal structure of objects.
When you need Order → Customer → Address → ZIP code, you’re not “using the domain.”
You’re forcing encapsulation open and spreading coupling where nobody sees it — until a refactor triggers a domino effect.
order.getShippingZipCode()), not a “path.”If your code depends on the route, it’s not robust — it’s hostage to structure.
| Getter + call on return | ⚠️ | order.getCustomer().buy() | “friend of a friend” |
| 2+ levels of chaining | 🚨 | order.getCustomer().getAddress().getZipCode() | dependent on the path |
| Intentional fluent API | ✅ | query.where().limit() | designed to be chained |
| Simple VO/DTO | ⚠️ | person.getAddress().getCity() | ok if it’s “transparent data” |
String zipCode = order.getCustomer().getAddress().getZipCode();var customer = order.getCustomer();
var address = customer.getAddress();
var zipCode = address.getZipCode();var zipCode = Optional.ofNullable(order.getCustomer())
.map(Customer::getAddress)
.map(Address::getZipCode)
.orElse(null);package com.luizdev.articles.lawofdemeter;
public class OrderPassThrough {
private final CustomerBetter customer;
public OrderPassThrough(CustomerBetter customer) {
this.customer = customer;
}
public String getShippingZipCode() {
return customer.getShippingZipCode(); // pass-through
}
}
order.getShippingStreet();
order.getShippingCity();
order.getShippingDistrict();
order.getShippingNumber();package com.luizdev.articles.lawofdemeter;
public class ShippingServicePassThrough {
public String getShippingZipCode(OrderPassThrough order) {
return order.getShippingZipCode(); // service only talks to Order
}
}
// the service envies Customer/Address
if (order.getCustomer().getAddress().getState().equals("PE")) {
applySpecialShipping();
}// instead of asking state to decide outside:
if (order.getCustomer().getAddress().getState().equals("PE")) ...
// tell intent:
if (order.deliversToPernambuco()) ...var city = person.getAddress().getCity();String name = " Ednaldo ".trim().toLowerCase();var names = list.stream()
.map(String::trim)
.map(String::toLowerCase)
.toList();var request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/items"))
.timeout(Duration.ofSeconds(2))
.header("Accept", "application/json")
.GET()
.build();package com.luizdev.articles.lawofdemeter;
public class Address {
private final String zipCode;
public Address(String zipCode) {
this.zipCode = zipCode;
}
public String getZipCode() {
return zipCode;
}
}
package com.luizdev.articles.lawofdemeter;
import com.luizdev.articles.lawofdemeter.Address;
public class Customer {
private final Address address;
public Customer(Address address) {
this.address = address;
}
public Address getAddress() {
return address;
}
}
package com.luizdev.articles.lawofdemeter;
import com.luizdev.articles.lawofdemeter.Customer;
public class Order {
private final Customer customer;
public Order(Customer customer) {
this.customer = customer;
}
public Customer getCustomer() {
return customer;
}
}
package com.luizdev.articles.lawofdemeter;
import com.luizdev.articles.lawofdemeter.Order;
public class ShippingService {
public String getShippingZipCode(Order order) {
return order.getCustomer().getAddress().getZipCode();
}
}
package com.luizdev.articles.lawofdemeter;
public class ShippingServiceBetter {
public String getShippingZipCode(OrderBetter order) {
return order.getShippingZipCode();
}
}
package com.luizdev.articles.lawofdemeter;
public class OrderBetter {
private final CustomerBetter customer;
public OrderBetter(CustomerBetter customer) {
this.customer = customer;
}
public String getShippingZipCode() {
return customer.getShippingZipCode();
}
}
package com.luizdev.articles.lawofdemeter;
public class CustomerBetter {
private final Address address;
public CustomerBetter(Address address) {
this.address = address;
}
public String getShippingZipCode() {
return address.getZipCode();
}
}
package com.luizdev.articles.lawofdemeter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.Test;
public class ShippingServiceTest {
@Test
void should_get_zip_code_for_shipping_calculation() {
Order order = mock(Order.class);
Customer customer = mock(Customer.class);
Address address = mock(Address.class);
when(order.getCustomer()).thenReturn(customer);
when(customer.getAddress()).thenReturn(address);
when(address.getZipCode()).thenReturn("10001");
ShippingService service = new ShippingService();
String zipCode = service.getShippingZipCode(order);
assertEquals("10001", zipCode);
}
}
return order.getShippingZipCode();package com.luizdev.articles.lawofdemeter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.Test;
public class ShippingServiceBetterTest {
@Test
void should_get_zip_code_for_shipping_calculation() {
OrderBetter order = mock(OrderBetter.class);
when(order.getShippingZipCode()).thenReturn("10001");
ShippingServiceBetter service = new ShippingServiceBetter();
String zipCode = service.getShippingZipCode(order);
assertEquals("10001", zipCode);
}
}
What is the core idea of the Law of Demeter?