Law of Demeter

Intermediate · 10 min read · ▶ live playground · ✦ checkpoint

One line completes a payment: order.getCustomer().getWallet().getAccount().pay(amount). Then Wallet stops exposing an account, and a checkout class four objects away fails to compile.

That chain is a train wreck: a run of calls where each returned object becomes the receiver of the next call. It lets the first caller navigate a graph of objects and depend on every link along the route.

The Law of Demeter (LoD) says a method should call methods only on itself, its parameters, objects it creates, and its direct fields—not on objects returned by other calls. The shorthand is “don't talk to strangers.” An object can ask a neighbor to do work; it should not reach through that neighbor to find someone deeper.

Before: checkout walks the object graph

The payment line reveals four domain types to CheckoutClient. To finish one action, the client must know that an order exposes a customer, a customer exposes a wallet, and a wallet exposes a bank account with pay().

class diagram

Before: the client reaches through every object

Before: the client reaches through every object
  1. 1CheckoutClient wants one result: charge the order's payment source.
  2. 2The client asks its parameter for a Customer instead of asking it to charge.
  3. 3The returned Customer exposes another object, so the client learns the next link.
  4. 4Wallet becomes a third type the distant client must navigate correctly.
  5. 5Only after four hops does the client reach the behavior it needed all along.

The dashed arrows trace temporary use along the expression. CheckoutClient starts with one legitimate parameter, order, but each getter hands it another object to command.

class BankAccount {
  private balance: number;
 
  public constructor(balance: number) {
    this.balance = balance;
  }
 
  public pay(amount: number): void {
    if (amount > this.balance) {
      throw new Error("insufficient funds");
    }
    this.balance -= amount;
  }
}
 
class Wallet {
  private account: BankAccount;
 
  public constructor(account: BankAccount) {
    this.account = account;
  }
 
  public getAccount(): BankAccount {
    return this.account;
  }
}
 
class Customer {
  private wallet: Wallet;
 
  public constructor(wallet: Wallet) {
    this.wallet = wallet;
  }
 
  public getWallet(): Wallet {
    return this.wallet;
  }
}
 
class Order {
  private customer: Customer;
 
  public constructor(customer: Customer) {
    this.customer = customer;
  }
 
  public getCustomer(): Customer {
    return this.customer;
  }
}
 
class CheckoutClient {
  public complete(order: Order, amount: number): void {
    // smell: this client depends on every type returned along the chain.
    order.getCustomer().getWallet().getAccount().pay(amount);
  }
}

Autocomplete makes the line easy to write, but the dependency is broad. Renaming Customer.getWallet(), replacing Wallet with stored payment methods, or changing account selection can all force an edit in CheckoutClient. Encapsulation is weak because the public getters expose navigation rather than useful intent.

After: tell the nearest object what should happen

Tell, Don't Ask means telling an object the result you want instead of asking for its internal state and making the decision outside. The client already knows an Order, so it says order.charge(amount). Each object delegates to its own direct field.

class diagram

After: each object speaks only to its neighbor

After: each object speaks only to its neighbor
  1. 1CheckoutClient keeps the same goal without learning the payment structure.
  2. 2The client tells its Order parameter to charge; its knowledge stops there.
  3. 3Order delegates to the Customer stored in its own direct field.
  4. 4Customer knows its Wallet, but it does not expose that object to the client.
  5. 5Wallet alone knows which BankAccount receives the final debit.

The internal object graph still exists. The difference is where knowledge lives. Each plain line represents one direct field, while the client has one dashed use arrow to its Order parameter.

class BankAccount {
  private balance: number;
 
  public constructor(balance: number) {
    this.balance = balance;
  }
 
  public debit(amount: number): void {
    if (amount > this.balance) {
      throw new Error("insufficient funds");
    }
    this.balance -= amount;
  }
 
  public remainingBalance(): number {
    return this.balance;
  }
}
 
class Wallet {
  private account: BankAccount;
 
  public constructor(account: BankAccount) {
    this.account = account;
  }
 
  public pay(amount: number): void {
    this.account.debit(amount);
  }
}
 
class Customer {
  private wallet: Wallet;
 
  public constructor(wallet: Wallet) {
    this.wallet = wallet;
  }
 
  public charge(amount: number): void {
    this.wallet.pay(amount);
  }
}
 
class Order {
  private customer: Customer;
 
  public constructor(customer: Customer) {
    this.customer = customer;
  }
 
  public charge(amount: number): void {
    this.customer.charge(amount);
  }
}
 
class CheckoutClient {
  public complete(order: Order, amount: number): void {
    order.charge(amount);
  }
}
 
const account = new BankAccount(100);
const order = new Order(new Customer(new Wallet(account)));
const checkout = new CheckoutClient();
 
checkout.complete(order, 30);
console.log(`Remaining balance: $${account.remainingBalance().toFixed(2)}`);

Remaining balance: $70.00

If wallet selection changes, Customer and Wallet negotiate that change near the data. CheckoutClient still asks an order to charge. The forwarding methods are small, but they protect boundaries: Order exposes business intent instead of handing out its customer for outside navigation.

LoD does not eliminate coupling. Order knows the Customer.charge() contract, and Customer knows Wallet.pay(). It localizes coupling so one object knows its immediate collaborator rather than the whole route.

When chaining is honest

A data transfer object (DTO) is a data-shaped value used to carry fields across a boundary. Reading response.customer.address.city may be reasonable when the value has no behavior to protect and its purpose is data transport. If that external shape changes often, map it once at the boundary rather than adding forwarding methods to every field.

A fluent builder is an object whose methods return the builder so a caller can configure one result through a readable chain. query.select("name").where("active").limit(10) talks to the same builder contract at each step; it is not walking from a query into the builder's private collaborators.

Collections and immutable records also invite field access or transformations that would look awkward behind command methods. LoD targets behavior coupling, not every dot in a program. Applying it mechanically can produce long rows of pass-through methods that hide plain data without protecting a decision.

Use the principle when a caller reaches through one behavior-rich object to command another. Let data remain data, and let fluent APIs chain when the chain is their intended public contract.

Checkpoint

Answer all three to mark this lesson complete

You can now keep object conversations among direct neighbors instead of exposing an entire graph. Next, noun and verb extraction turns a written requirement into the first set of classes and responsibilities.

+50 XP on completion