Loading content...
A senior engineer once told me: "The worst code I've ever seen was written by developers who learned design principles but not when to apply them."
The Law of Demeter, like all design principles, is a guideline — not a commandment carved in stone. Applied dogmatically, it can lead to absurd code: objects with hundreds of delegation methods, indirection layers that obscure meaning, and designs that are technically "compliant" but practically unmaintainable.
Applied thoughtfully, LoD creates systems that are genuinely more resilient, testable, and evolvable. The difference lies in understanding why the principle exists and recognizing when its benefits are outweighed by its costs.
This page develops your judgment for applying LoD appropriately. You'll learn where LoD is most valuable, where its application may be relaxed, how to avoid common LoD anti-patterns, and how to balance LoD with other design concerns. By the end, you'll approach LoD as a tool to wield skillfully, not a rule to follow blindly.
Not all code equally benefits from LoD. The principle provides the most value in contexts where its coupling-reduction effect addresses real problems. Understanding these contexts helps you prioritize LoD application where it matters most.
| Factor | Low LoD Value | High LoD Value |
|---|---|---|
| Codebase lifetime | Prototype, short-term project | Production system, multi-year lifespan |
| Rate of change | Stable, rarely modified | Actively evolving requirements |
| Team structure | Single developer, small team | Multiple teams, organizational boundaries |
| API scope | Internal implementation detail | Public API, cross-module interface |
| Test coverage | Minimal or no tests | Extensive test suite depends on interfaces |
| Refactoring frequency | Rare restructuring expected | Regular internal refactoring expected |
Apply LoD rigor at boundaries and public interfaces. Be more pragmatic in implementation details that are contained within a single module and changed together. The cost of LoD enforcement should match the benefit of coupling reduction for that specific context.
Recognizing contexts where strict LoD application provides little value (or creates problems) is equally important. These are situations where the coupling concerns LoD addresses are already managed in other ways, or where the overhead of LoD compliance outweighs benefits.
config.database.connection.timeout is accessing configuration, not navigating domain objects.12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
// DTO Navigation — Acceptable// DTOs are data containers; their structure IS their interfaceinterface OrderDto { customer: CustomerDto; items: ItemDto[]; shipping: ShippingDto;} function displayOrder(dto: OrderDto): void { // Navigating DTO structure is expected — DTOs are data, not behavior console.log(`Customer: ${dto.customer.name}`); console.log(`Items: ${dto.items.length}`); console.log(`Ship to: ${dto.shipping.address.city}`);} // Configuration Navigation — Acceptable// Config structure is stable and environment-definedinterface AppConfig { database: { host: string; port: number; connection: { timeout: number; poolSize: number; }; };} function initDatabase(config: AppConfig): void { // Config navigation is standard; structure matches file/env format const timeout = config.database.connection.timeout; // ...} // Test Assertion Navigation — Acceptable// Tests verify internal state by designdescribe('Order', () => { it('stores customer correctly', () => { const order = new Order(customer, items); // Test navigates to verify internal state expect(order.getCustomer().getAddress().getCity()) .toBe('San Francisco'); });}); // Internal Cohesion — Acceptable at Lower Scale// Within a single cohesive module, tight coupling is expectedclass OrderAggregate { private order: Order; private payment: Payment; private shipping: Shipping; // These classes all change together, live in same module // Internal coupling is acceptable calculateTotal(): Money { return this.order.subtotal() .add(this.shipping.getCost()) .subtract(this.payment.getAppliedDiscounts()); }}Relaxing LoD should be a conscious decision with clear justification, not an excuse for sloppy code. Ask yourself: "Why doesn't LoD apply here?" If you can't articulate a reason from the list above, you're likely just avoiding the refactoring effort.
The most common LoD anti-pattern is delegation method explosion: creating hundreds of pass-through methods that merely forward calls to internal objects. While technically LoD-compliant, this creates worse problems than the violations it prevents.
123456789101112131415161718192021222324252627282930313233343536373839
// ❌ ANTI-PATTERN: Delegation Method Explosion// Every Customer method needs a corresponding Order method class Customer { getName(): string { ... } getEmail(): string { ... } getPhone(): string { ... } getAddress(): Address { ... } getShippingAddress(): Address { ... } getBillingAddress(): Address { ... } getPreferences(): Preferences { ... } getPaymentMethods(): PaymentMethod[] { ... } getLoyaltyPoints(): number { ... } getMembershipTier(): string { ... } getCreatedAt(): Date { ... } getLastOrderDate(): Date { ... } // ... 20 more methods} class Order { private customer: Customer; // Now Order has to expose ALL of these getCustomerName(): string { return this.customer.getName(); } getCustomerEmail(): string { return this.customer.getEmail(); } getCustomerPhone(): string { return this.customer.getPhone(); } getCustomerAddress(): Address { return this.customer.getAddress(); } getCustomerShippingAddress(): Address { return this.customer.getShippingAddress(); } getCustomerBillingAddress(): Address { return this.customer.getBillingAddress(); } getCustomerPreferences(): Preferences { return this.customer.getPreferences(); } getCustomerPaymentMethods(): PaymentMethod[] { return this.customer.getPaymentMethods(); } getCustomerLoyaltyPoints(): number { return this.customer.getLoyaltyPoints(); } getCustomerMembershipTier(): string { return this.customer.getMembershipTier(); } getCustomerCreatedAt(): Date { return this.customer.getCreatedAt(); } getCustomerLastOrderDate(): Date { return this.customer.getLastOrderDate(); } // ... 20 more delegation methods // Order's actual behavior is lost in a sea of delegations}Why this is harmful:
Instead of mirroring internal structure, ask: "What behaviors do callers actually need from Order?" Create methods for those behaviors — not pass-throughs for every internal accessor. If callers need general Customer access frequently, perhaps Order should expose getCustomer() directly.
12345678910111213141516171819202122232425262728293031323334353637
// ✅ BETTER: Behavior-Focused Delegation// Order exposes what it DOES, not what it CONTAINS class Order { private customer: Customer; // Expose the FEW behaviors Order callers actually need sendConfirmation(): void { this.notificationService.send( this.customer.getEmail(), this.createConfirmationMessage() ); } getShippingDestination(): ShippingDestination { return new ShippingDestination( this.customer.getName(), this.customer.getShippingAddress() ); } calculateLoyaltyDiscount(): Money { return this.loyaltyProgram.calculateDiscount( this.customer.getLoyaltyPoints(), this.subtotal ); } // For cases where Customer access is genuinely needed // consider exposing Customer directly getCustomer(): Customer { return this.customer; // This is OK if callers work with customers }} // Key insight: Order has 4 intentional points of delegation// instead of 20+ mechanical mirror methodsAnother LoD anti-pattern is wrapper overload: creating layer upon layer of wrapper objects solely to avoid direct access. Each layer adds indirection without adding value, making the code harder to understand and maintain.
12345678910111213141516171819202122232425262728293031323334353637
// ❌ ANTI-PATTERN: Wrapper Overload// Unnecessary layers just to avoid 'getCustomer().getAddress()' // Original structureorder.getCustomer().getAddress().getCity(); // "LoD-compliant" wrapper approach (wrong)class OrderCustomerWrapper { constructor(private customer: Customer) {} getAddressWrapper(): CustomerAddressWrapper { return new CustomerAddressWrapper(this.customer.getAddress()); }} class CustomerAddressWrapper { constructor(private address: Address) {} getCity(): string { return this.address.getCity(); }} class Order { getCustomerWrapper(): OrderCustomerWrapper { return new OrderCustomerWrapper(this.customer); }} // Usageorder.getCustomerWrapper().getAddressWrapper().getCity(); // What went wrong?// - Same navigation, just more complex// - Added maintenance burden (3 new classes)// - Coupling not reduced, just obscured// - Worse readability than original1234567891011121314151617181920212223242526
// ✅ CORRECT APPROACH: Proper Encapsulation// Identify the actual behavior needed and encapsulate it class Order { // What behavior does the caller actually need? // They need the shipping city for zone calculation getShippingCity(): string { return this.customer.getShippingCity(); }} class Customer { getShippingCity(): string { return this.address.getCity(); }} // Usageorder.getShippingCity(); // What's better?// - No new wrapper classes// - Clear, intentional API// - Behavior is where it belongs// - Internal structure is hiddenWrappers that add value (adapters for external APIs, decorators for behavior modification, facades for simplification) are legitimate. Wrappers that merely hide navigation without adding behavior are overhead. Ask: "Does this wrapper DO anything, or just forward calls?"
Design principles can conflict. Strict LoD adherence might violate simplicity (KISS), create premature abstraction (YAGNI), or require restructuring that conflicts with cohesion. Balancing these principles requires judgment about which concerns matter most in each specific context.
| Principle | Potential Conflict | Balance Strategy |
|---|---|---|
| KISS (Keep It Simple) | Delegation methods add complexity | Only add delegations for frequently-used behaviors |
| YAGNI (You Aren't Gonna Need It) | Preemptive LoD compliance may over-engineer | Apply LoD as coupling problems emerge, not speculatively |
| DRY (Don't Repeat Yourself) | Multiple delegation methods may duplicate structure | Use parameter objects or interfaces to factor common needs |
| Single Responsibility | LoD may cause objects to know about more concerns | Ensure delegations align with object's core responsibility |
| Performance | Delegation adds call overhead | In hot paths, consider acceptable violations with documentation |
| Readability | Deep delegation chains obscure what's happening | Balance encapsulation with clarity; document complex flows |
1234567891011121314151617181920212223242526272829303132333435363738
// LoD vs KISS// LoD-pure but complexorder.getConfirmationDetails().getRecipient().getEmailAddress(); // KISS-preferred, mild LoD bendconst customer = order.getCustomer();sendConfirmation(customer.getEmail(), customer.getName()); // LoD vs YAGNI// Premature LoD compliance (YAGNI violation)class Order { // Added "just in case" someone needs logging format getCustomerLogFormat(): string { ... } // Added "just in case" someone needs short format getCustomerShortName(): string { ... } // Added "just in case"... getCustomerFullDetails(): CustomerDetails { ... }} // YAGNI-compliant approachclass Order { // Add delegation methods when actual callers need them // Until then, expose getCustomer() if needed getCustomer(): Customer { return this.customer; }} // LoD vs Performance (rare but real)// In a tight loop processing millions of recordsfor (const record of millionRecords) { // LoD-compliant: multiple method calls process(record.getX(), record.getY(), record.getZ()); // Performance-optimized: direct access (with documentation) // Note: Violating LoD for performance in hot path process(record.data.x, record.data.y, record.data.z);}When principles conflict, consider: (1) What problem actually exists now? (2) What's the cost of each approach? (3) What's most likely to change? Favor the approach that solves real problems with minimal ceremony. Design principles exist to serve good design, not the other way around.
Between strict LoD enforcement and complete abandonment lies a practical middle ground that most successful codebases inhabit. This approach applies LoD thoughtfully based on context rather than dogmatically.
order.getCustomer().getName() is usually fine. order.getCustomer().getAddress().getCity().getShippingZone() is not. The deeper you go, the more coupling you create.order.getCustomer().sendNotification() is better than order.getCustomer().getEmail() then manually sending. Push behavior to data.thisAsk yourself: "If the internal structure of this object changed, would I be surprised if my code broke?" If yes, you're probably navigating too deeply. If no (because the structure is stable, intentionally exposed, or within your own module), the navigation may be acceptable.
LoD was formulated for object-oriented programming, but its core insight — limiting knowledge between components — applies across paradigms. Understanding how LoD translates helps you apply its principles consistently regardless of the technology stack.
12345678910111213141516171819202122232425
// LoD in Functional Style // ❌ LoD violation: function reaches into passed structurefunction formatShipping(order: Order): string { // Reaching into order's structure const city = order.customer.address.city; return `Ships to: ${city}`;} // ✅ LoD compliant: function receives what it needsfunction formatShipping(cityName: string): string { return `Ships to: ${cityName}`;} // Caller provides the value:formatShipping(order.getShippingCityName()); // Alternative: use lenses for structured accessconst shippingCityLens = compose( orderCustomerLens, customerAddressLens, addressCityLens); const cityName = view(shippingCityLens, order);12345678910111213
❌ LoD Violation at Service Level:OrderService → CustomerService → AddressService → GeoService OrderService orchestrates calls across three services to get shipping zoneAny service API change breaks the entire flow ✅ LoD Compliant at Service Level:OrderService → ShippingService ShippingService internally aggregates what it needs from other servicesOrderService only knows about ShippingServiceInternal service changes don't affect OrderServiceRegardless of paradigm, LoD's essence is: components should have minimal knowledge of other components' internal structure. Whether you're navigating objects, reaching into data structures, or orchestrating service calls, limiting what each component "knows" reduces coupling and increases flexibility.
When reviewing code for LoD compliance, use this systematic checklist. It helps distinguish problematic violations from acceptable patterns and provides actionable feedback.
12345678910111213141516171819202122232425262728293031
// Review Example 1order.getCustomer().getEmail(); // Checklist application:// 1. Chain length: 2 levels ✓// 2. Purpose: Navigation (accessing email from customer) ⚠️// 3. Boundary: Depends on context// 4. Stability: Customer-has-Email is stable ✓// 5. Alternative: Could Order provide getCustomerEmail()? Maybe.// 6. Test impact: Low — structure is stable// 7. Pattern: Check if repeated — if so, add delegation// 8. Documentation: N/A // VERDICT: Borderline. Acceptable in implementation code,// worth adding delegation if used frequently. // Review Example 2order.getCustomer().getWallet().getCreditCard().getNetwork().validateForMerchant(); // Checklist application:// 1. Chain length: 5 levels ❌// 2. Purpose: Deep navigation ❌// 3. Boundary: Likely crossing domain boundaries// 4. Stability: Wallet/CreditCard/Network structure is volatile// 5. Alternative: order.validatePaymentMethod() would encapsulate ✓// 6. Test impact: Would require complex mock setup// 7. Pattern: Payment validation likely repeated// 8. Documentation: Not documented as intentional // VERDICT: Clear violation. Refactor to encapsulate validation.We've explored the nuanced art of applying the Law of Demeter appropriately. The goal is not blind adherence but informed judgment — understanding when LoD provides value and when its costs outweigh benefits.
You've completed the Law of Demeter module. You now understand: the formal definition and rationale ("talk only to friends"), how LoD reduces coupling through firebreaks, the distinction between navigation chains (violations) and transformation chains (compliant), and how to apply LoD appropriately based on context. This principle, combined with the others in this chapter, forms a foundation for designing maintainable, evolvable object-oriented systems.