Skip to content
aipatterns.dev

Code smellCouplers

Message Chains

Client code walks a chain like a.getB().getC().getD() to reach what it needs, coupling itself to every link in the path.

Also known as
Train Wreck, Law of Demeter violation

Signs

  • Expressions such as order.customer.account.billingAddress.country appear in client code.
  • The same navigation path is repeated in several files.
  • Null checks guard each step: a?.b?.c?.d.
  • A change to how Account stores addresses breaks code that only wanted a country code.
  • Tests must build a deep object graph to exercise one line.

Why it happens

  • The object model mirrors the database schema, and callers navigate it the way they would join tables.
  • Getters exist for every field, so following them is the path of least resistance.
  • Nobody asked the first object for what the client actually needed, so no method for it exists.

Example

A tax calculator needs the buyer's country and digs for it.

before.ts
class Address {
  constructor(readonly country: string) {}
}
class Account {
  constructor(readonly billingAddress: Address) {}
}
class Customer {
  constructor(readonly account: Account) {}
}
class Order {
  constructor(readonly customer: Customer, readonly subtotal: number) {}
}

function vatFor(order: Order): number {
  const country = order.customer.account.billingAddress.country;
  return country === "DE" ? order.subtotal * 0.19 : 0;
}

function shippingZone(order: Order): string {
  return order.customer.account.billingAddress.country === "US" ? "domestic" : "intl";
}

Both functions know the full path from Order to Address.

after.ts
class Customer {
  constructor(private readonly account: Account) {}
  billingCountry(): string {
    return this.account.billingAddress.country;
  }
}
class Order {
  constructor(private readonly customer: Customer, readonly subtotal: number) {}
  billingCountry(): string {
    return this.customer.billingCountry();
  }
}

function vatFor(order: Order): number {
  return order.billingCountry() === "DE" ? order.subtotal * 0.19 : 0;
}

function shippingZone(order: Order): string {
  return order.billingCountry() === "US" ? "domestic" : "intl";
}

Clients now depend on Order alone. Only Customer knows about Account.

Treatment

  • If clients repeat a chain to reach one value, use Hide Delegate on the first link so it answers directly.
  • If the client does work with the value at the end of the chain, use Extract Method on that work, then Move Method it to an object near the start of the chain.
  • If you find yourself hiding delegates on every class, stop. That produces Middle Man. Hide only the paths clients actually use.

Payoff

  • Clients depend on one neighbor instead of the whole object graph.
  • Restructuring intermediate objects stops breaking distant code.
  • Tests need shallower fixtures.

When to ignore

  • Fluent and builder APIs return this or a value of the same kind: query.where(...).orderBy(...).limit(10). That is one object, not a chain of strangers.
  • Pipelines over plain values, such as items.filter(...).map(...), are transformations, not navigation.
  • Immutable records and read models you only read, such as parsed JSON or configuration, where the shape is the contract. Wrapping every path adds methods without reducing coupling.
  • Hiding every link would turn each class into a forwarding layer. Prefer the occasional chain.

Agent checklist

  • Confirm the chain crosses several distinct types, not a fluent API on one type.
  • Count how many call sites use the same path before adding a delegating method.
  • Name the new method for what the client wants (billingCountry), not for the path.
  • Reduce visibility of the intermediate getters once no client uses them.
  • Avoid creating a Middle Man with many pure forwarders.

Treatments

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.