Train wreck anti-pattern
Learn why chaining method calls across object boundaries violates the Law of Demeter, creates brittle coupling, and how delegating methods fix the problem cleanly.
A train wreck is a structural coupling smell: a caller navigates through several distinct objects to reach data or behavior owned deeper in the object graph. The problem is not the number of dots by itself; it is that the caller knows too much about intermediate relationships and their representation.
TL;DR
- A train wreck is a chain like
order.getCustomer().getAddress().getCity()where each dot crosses an object boundary, coupling the caller to the internal structure of every type in the chain. - The Law of Demeter says: only talk to your immediate neighbors. Do not navigate through friends to reach strangers.
- Each link in the chain is a dependency. If
Addressis refactored toLocation, every chain touching.getAddress()breaks. - The fix is delegating methods:
order.getShippingCity()hides the chain behind a single call on the nearest object. - Fluent APIs and builder chains are NOT train wrecks because they return the same object, not progressively deeper objects.
The Problem
A train wreck often shows up as long chains, repeated graph navigation, and tests that mock every intermediate object. It matters because a change to an inner type can break callers that should only depend on the outer object's behavior, and because the chain can expose data that the caller does not need to know how to obtain.
Concrete Example
Your ShippingService calculates delivery costs. To get the customer's city and shipping zone, it navigates through Order to Customer to Address:
public class ShippingService {
public ShippingQuote calculateShipping(Order order) {
// 3-level chain: Order -> Customer -> Address
String city = order.getCustomer().getAddress().getCity();
String zone = order.getCustomer().getAddress().getShippingZone();
String country = order.getCustomer().getAddress().getCountry();
// 2-level chain: Warehouse -> Location
String warehouseRegion = closestWarehouse.getLocation().getRegion();
return rateCalculator.quote(city, zone, country, warehouseRegion);
}
}
This method depends on five classes: Order, Customer, Address, Warehouse, and Location. The method signature says it needs an Order, but at runtime it reaches three levels deep. When the team renames Address to ContactInfo or nests Address inside a ShippingDetails wrapper, every chain breaks.
Moving Address from Customer to a new ShippingProfile class can produce dozens of compilation errors across services when callers contain the full chain. Those errors are evidence that the object graph leaked across a boundary.
The method "reaches through" each object to get to the next. Every intermediate type becomes a compile-time dependency that the caller should never have known about.
Why It Happens
- Object graph navigation feels natural. Developers think in terms of the domain: "an order has a customer who has an address." Translating that directly to code produces chains.
- Getters expose the graph. If
CustomerhasgetAddress()andAddresshasgetCity(), chaining them is the path of least resistance. The getter API invites deep navigation. - Delegation feels like boilerplate. Writing
order.getShippingCity()that delegates tocustomer.getShippingCity()that delegates toaddress.getCity()feels like useless indirection, until a refactoring proves its value. - Flat service classes. In anemic-model codebases, services do all the work and entities are pure data holders. The service must navigate the graph because the entities offer no behavior.
The formal Law of Demeter
A method M of object O should only call methods on: (1) O itself, (2) M's parameters, (3) objects created within M, (4) O's direct fields. Anything reached via a return value of another call is a "stranger" and should not be accessed.
How to Detect It
Diagnose the smell by counting the distinct domain types a caller traverses and by checking whether tests must stub the entire path just to exercise one behavior.
| Signal | What It Means | How to Check |
|---|---|---|
| 2+ consecutive dots on the same expression | Caller navigates through intermediate objects | Regex: \.\w+\(\)\.\w+\(\)\.\w+\(\) in source |
| Method imports types it never declares | The chain pulls in transitive types | Check import statements vs parameter types |
| Refactoring an inner class breaks distant callers | Callers depend on internal structure | Rename an inner class and count compile errors |
| Mocking setup requires 3+ layers of stubs | Tests must mock every object in the chain | Check when(...).thenReturn(...) depth in tests |
| Same chain appears in multiple classes | Copy-paste coupling through navigation | Search for repeated multi-dot patterns |
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.
Related Articles
Learn why methods that obsess over another class's data signal misplaced responsibility, and how Move Method refactoring creates cleaner, more cohesive designs.
Learn why a class that knows too much and does too many unrelated things is hard to test, extend, and maintain, and how to break it apart using Single Responsibility and Extract Class.