DEVBLUEPRINTS

Blog

  • About the blog
  • Archive
  • Newsletter
  • RSS

Legal

  • Terms & Privacy
  • Contact
  • About me

Subscribe to the Newsletter

I authorize communications by email or other means and agree to the Terms and Privacy Policy

 

Blog
  • About the blog
  • Archive
  • Newsletter
  • RSS
Legal
  • Terms & Privacy
  • Contact
  • About me
© 2026 All rights reserved — Designed and built withFooter Heartpor Ednaldo Luiz
Blog/Back-end

Law of Demeter: reduce coupling and stop chaining

Enough “train wrecks”: apply the Law of Demeter to cut coupling, simplify tests, and protect your domain. See when to use it (and when to ignore it).

software design
java
object calisthenics
clean code
law of demeter
design principles
oop
Law of Demeter: reduce coupling and stop chaining
Ednaldo Luiz
Ednaldo Luiz
Level: Intermediate
Level:
Published: 06 de fevereiro de 2026
Last updated: 06 de fevereiro de 2026

On this page

Share

Recommended

Related content to this article.

Ednaldo Luiz
GitHub
LinkedIn
Portfólio

Ednaldo Luiz

Software Architect and Engineer | Java & AI

Software Engineer focused on architecture and performance. I work with Java/Spring Boot, well-structured SQL, scalable services on AWS, and GenAI solutions with RAG (LangChain + vector databases). I value readable code and well-justified decisions.
12 min read
views: - views

Introduction

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.

Source: Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship, Chapter 6 (Objects and Data Structures), section "The Law of Demeter".

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).


What is the Law of Demeter

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:

  • “navigating” through objects (enter, enter again… until you reach the data)
  • spreading knowledge about the domain’s internal structure across the system
  • domino effect: a refactor in the middle breaks code far away
  • turning the domain into getters + scattered logic (anemic domain: classes that only carry data, with no business behavior)

Immediate friends

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?”

Who is a friend?

If you want a fast way to decide “friend” vs “friend of a friend,” use this table:

SituationIs it a friend?ExampleWhy
this✅this.validate()it’s you
Direct field✅this.customeryou 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.

Violation signals

Even “pretty” code can hide path dependency.

1) Train wreck (the classic)

Chains of calls like this are generally considered to be sloppy style and should be avoided.

Source: Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship, Chapter 6, section "Train Wrecks".

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.

2) “I split it into variables” (still chaining)

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.”

3) Chained Optional (the elegant disguise)

Syntactically clean. In reality? Same dependency chain. The delivery service still knows the whole route to the ZIP code.

4) “Pass-through method” (middle man)

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.

Wrong (secretary: just tunnels)

You start stuffing Order with “navigation shortcuts”:

The API grows, maintenance gets worse, and you just moved the chain somewhere else.

Right (intent method)

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.

Demeter doesn’t want you to hide dots. It wants you to hide dependency.

Signals that usually come together

When you see a train wreck, two other smells often show up right next to it.

Feature Envy

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.

Tell, Don’t Ask

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.

Source: Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship, Chapter 6, section about hiding structure (after the scratch directory example).

Code review rule: if you pull data to decide outside, that probably should become an intent method in the domain.


Conscious exceptions

Whether this is a violation of the Law of Demeter depends on whether or not these are objects or data structures.

Source: Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship, Chapter 6, section "Train Wrecks".

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:

Value Objects / simple DTOs

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.

Standard library / stable types

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.

Intentional fluent 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:

  • Is this behavioral domain (entity/aggregate) or data (VO/DTO)?
  • Should the caller even know this path exists?
  • Can “get data and decide outside” become an intent method?
  • If I change Address, how many files break?

Full example

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.

Building the model

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.

The problem

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.

The solution

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.


Demeter in tests

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.

The problem: mock tree

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.

The solution: intent method

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.

Practical rule

  • If, to test a method, you must mock 3+ objects just to reach a single piece of data, you probably broke Demeter.
  • If an internal refactor (e.g., changing Address) breaks “business rule” tests, your tests are coupled to the access path, not behavior.
  • Prefer intent methods that reduce dependencies for callers and for tests.

Conclusion

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.

  • Prefer an intent method (order.getShippingZipCode()), not a “path.”
  • If the test needs a mock tree just to get data, Demeter is broken.
  • If it’s a domain with invariants, be strict.
  • If it’s a transparent VO/DTO or an intentional fluent API, relax — don’t become the dot police.

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();
String zipCode = order.getCustomer().getAddress().getZipCode();
var customer = order.getCustomer();
var address = customer.getAddress();
var zipCode = address.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);
var zipCode = Optional.ofNullable(order.getCustomer())
	.map(Customer::getAddress)
	.map(Address::getZipCode)
	.orElse(null);
OrderPassThrough.java
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
	}
}
OrderPassThrough.java
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();
order.getShippingStreet();
order.getShippingCity();
order.getShippingDistrict();
order.getShippingNumber();
ShippingServicePassThrough.java
package com.luizdev.articles.lawofdemeter;

public class ShippingServicePassThrough {

	public String getShippingZipCode(OrderPassThrough order) {
		return order.getShippingZipCode(); // service only talks to Order
	}
}
ShippingServicePassThrough.java
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();
}
// 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()) ...
// instead of asking state to decide outside:
if (order.getCustomer().getAddress().getState().equals("PE")) ...

// tell intent:
if (order.deliversToPernambuco()) ...
ReadFromVO.java
var city = person.getAddress().getCity();
ReadFromVO.java
var city = person.getAddress().getCity();
String name = "  Ednaldo  ".trim().toLowerCase();
String name = "  Ednaldo  ".trim().toLowerCase();
var names = list.stream()
	.map(String::trim)
	.map(String::toLowerCase)
	.toList();
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();
var request = HttpRequest.newBuilder()
	.uri(URI.create("https://api.example.com/items"))
	.timeout(Duration.ofSeconds(2))
	.header("Accept", "application/json")
	.GET()
	.build();
Address.java
package com.luizdev.articles.lawofdemeter;

public class Address {
		
	private final String zipCode;

	public Address(String zipCode) {
		this.zipCode = zipCode;
	}

	public String getZipCode() {
		return zipCode;
	}
}
Address.java
package com.luizdev.articles.lawofdemeter;

public class Address {
		
	private final String zipCode;

	public Address(String zipCode) {
		this.zipCode = zipCode;
	}

	public String getZipCode() {
		return zipCode;
	}
}
Customer.java
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;
	}
}
Customer.java
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;
	}
}
Order.java
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;
	}
}
Order.java
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;
	}
}
ShippingService.java
package com.luizdev.articles.lawofdemeter;

import com.luizdev.articles.lawofdemeter.Order;

public class ShippingService {
		
	public String getShippingZipCode(Order order) {
		return order.getCustomer().getAddress().getZipCode();
	}
}
ShippingService.java
package com.luizdev.articles.lawofdemeter;

import com.luizdev.articles.lawofdemeter.Order;

public class ShippingService {
		
	public String getShippingZipCode(Order order) {
		return order.getCustomer().getAddress().getZipCode();
	}
}
ShippingServiceBetter.java
package com.luizdev.articles.lawofdemeter;

public class ShippingServiceBetter {

	public String getShippingZipCode(OrderBetter order) {
		return order.getShippingZipCode();
	}
}
ShippingServiceBetter.java
package com.luizdev.articles.lawofdemeter;

public class ShippingServiceBetter {

	public String getShippingZipCode(OrderBetter order) {
		return order.getShippingZipCode();
	}
}
OrderBetter.java
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();
	}
}
OrderBetter.java
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();
	}
}
CustomerBetter.java
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();
	}
}
CustomerBetter.java
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();
	}
}
ShippingServiceTest.java
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);
	}
}
ShippingServiceTest.java
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();
return order.getShippingZipCode();
ShippingServiceBetterTest.java
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);
	}
}
ShippingServiceBetterTest.java
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);
	}
}
Question1/5

What is the core idea of the Law of Demeter?

ABC
A depends on B to reach C: the caller crosses internal objects to get what it wants.
ABCOKNot recommendedA is a"friend" of BB is a"friend" of CA friend of afriend is astranger
ServiceOrderCustomerAddress
Service talks to Order; Order talks to Customer; Customer talks to Address. No dependency on the internal path.