open atlas
↑ Back to track
Code patterns & craft CP · 09 · 02

Message chains & middle man

Message chains couple a caller to a whole object graph; middle man is the over-correction that delegates everything and earns nothing. Hide delegate, remove middle man, and tell-don't-ask are the cures — but Demeter pulls both ways, so the senior call is balance, not zealotry.

CP Senior ◷ 20 min
Level
FoundationsJuniorMiddleSenior

You write order.getCustomer().getAddress().getCity().getName() and it compiles, it ships, it works. Six months later someone flattens Address into Customer, and that one expression breaks in forty files — each of which had quietly memorised the exact shape of a graph it never owned. The chain worked. The problem was never correctness; it was that the caller knew the internal route through four objects to reach one string.

Then a teammate “fixes” it by wrapping everything: Order grows getCity(), which calls customer.getCity(), which calls address.getCity(). Now no chain is visible — and you’ve built a tower of classes that do nothing but forward calls. You traded one smell for its mirror image. This lesson is about both smells, their cures, and the judgment call that sits between them.

Goal

After this lesson you can recognise a message chain (a.getB().getC().getD()) as structural coupling to an object graph; recognise a middle man as a class whose methods only delegate; apply the three cures — hide delegate, remove middle man, tell-don’t-ask — to the right one; explain why the Law of Demeter pulls these two smells in opposite directions; and make the senior call about how much delegation is healthy encapsulation versus pass-through noise.

1

A message chain couples the caller to the entire path, not just the endpoint. When code reads a.getB().getC().getD(), the caller has hard-coded the shape of the graph: that A exposes a B, that B exposes a C, that C exposes a D. Every intermediate type is now part of the caller’s compile-time contract, even though the caller only wanted the final value.

// The caller "knows" Order → Customer → Address → string
const city = order.getCustomer().getAddress().getCity();

The blast radius is the length of the chain. Change any link — rename getAddress, make Customer.address nullable, move city up to Customer — and every call site that walked through it breaks. The caller paid for knowledge it never needed: it wanted a city, but it bought a map of three objects’ internals.

2

Hide delegate: ask the nearest object for what you want, and let it walk the rest of the chain. The cure for a message chain is usually hide delegate — push the navigation behind the object you already hold, so the caller depends only on its immediate neighbour.

class Order {
  constructor(private customer: Customer) {}
  // the caller's "one friend" exposes the endpoint directly
  get shippingCity(): string {
    return this.customer.address.city; // Order owns this walk; the caller doesn't
  }
}

// caller now touches one type, not three
const city = order.shippingCity;

This is the Law of Demeter (“only talk to your immediate friends”) applied as a refactoring: the caller talks only to Order. If Address later folds into Customer, the only place that breaks is inside Order.shippingCity — one edit, not forty. The chain didn’t disappear; it moved to the object that legitimately owns the relationship.

3

Middle man is hide-delegate taken too far: a class whose methods only forward. Apply hide delegate to everything and you get the opposite smell. If half of Order’s methods are one-line forwards to customer, Order has become a middle man — a pass-through tax. Every new method on Customer now needs a twin delegating method on Order, and every reader has to hop through the forward to find out where the work actually happens.

class Order {
  constructor(private customer: Customer) {}
  // none of this does anything but forward — pure pass-through
  get name() { return this.customer.name; }
  get email() { return this.customer.email; }
  get city() { return this.customer.address.city; }
  get phone() { return this.customer.phone; }
}

The cure is remove middle man: delete the forwarding methods and let callers talk to the real object — order.customer.email. Trading a chain for an empty wrapper isn’t progress; you’ve added a class to maintain that carries no behaviour of its own.

4

Tell-don’t-ask often dissolves both smells at once. Chains and middle men frequently appear because the caller asks for data and then acts on it elsewhere — pulling the city out to compute shipping somewhere far from the data. Move the behaviour to the data instead: tell the object to do the work.

// ASK: caller reaches in, gets data, decides — chain + logic far from data
const city = order.getCustomer().getAddress().getCity();
const fee = isRemote(city) ? 15 : 5;

// TELL: the object that owns the data answers the real question
class Order {
  shippingFee(): number {
    return this.customer.isRemote() ? 15 : 5; // graph stays private
  }
}
const fee = order.shippingFee();

Now no caller walks the graph and no wrapper exists purely to forward getCity. The senior move is to ask why the caller wanted that deep value at all — often the honest answer is a method that should have lived on the object, not a navigation that needs hiding.

5

The judgment: balance, not zealotry — Demeter taken to the limit manufactures middle men. Here is the tension the two smells expose. The Law of Demeter says “don’t reach through objects,” which pushes you toward hide delegate. But mechanically obeying it for every access means wrapping every neighbour’s neighbour in a forwarding method — which is exactly how you create middle men. The two smells pull against each other: zero delegation gives you brittle chains; total delegation gives you pass-through towers.

So the rule is not “eliminate all chains” or “eliminate all delegation.” Some delegation is healthy encapsulation — it hides a relationship the caller has no business knowing. Too much is noise — wrappers that hide nothing because the relationship is part of the public model anyway (a list.length, a Promise.then, a fluent builder are chains you should leave alone). The skill is reading which is which: does this delegation hide something likely to change, or does it just relay something already public?

Worked example

Walk a chain to a wrapper and back to balance. Start with a chain in a notifier that has no business knowing customer internals:

// notifier.ts — couples to Order → Customer → Address → string
function sendShippedEmail(order: Order) {
  const to = order.getCustomer().getEmail();
  const city = order.getCustomer().getAddress().getCity();
  mailer.send(to, `Your order ships to ${city}`);
}

Two chains, both walking through Customer. The naive over-correction is to wrap every hop on Order:

class Order {
  get customerEmail() { return this.customer.email; }
  get customerCity()  { return this.customer.address.city; }
  get customerName()  { return this.customer.name; }   // and on, and on...
}

Order is now a middle man — those getters carry no behaviour, and each new field on Customer demands a new forwarder. Better: keep the two delegations the notifier actually needs because they hide a relationship that may change (where city lives), and stop there:

class Order {
  // hides the Customer→Address walk that the notifier shouldn't know
  get shippingEmail(): string { return this.customer.email; }
  get shippingCity(): string  { return this.customer.address.city; }
}

function sendShippedEmail(order: Order) {
  mailer.send(order.shippingEmail, `Your order ships to ${order.shippingCity}`);
}

The notifier talks to one type. We added exactly two delegates — the ones that hide a likely-to-change graph — and refused the rest. If product later asks “email subject depends on whether it’s a remote region,” the next step is tell-don’t-ask: an order.isRemoteShipment() that keeps the decision next to the data. The point isn’t “no chains” or “wrap everything” — it’s: hide the relationships that will move, expose nothing extra.

Why this works

Why is “talk only to your immediate friends” framed as coupling, not etiquette? Because each . in a chain is a fact the caller now depends on. a.b.c.d encodes four facts: A-has-B, B-has-C, C-has-D, D-is-what-I-want. Three of those facts are about objects the caller never wanted to know. The Law of Demeter is a coupling budget: it caps how many of a system’s internal relationships any one caller is allowed to depend on. Hide delegate spends that budget where it pays — confining a likely change to one owner — rather than spreading the graph’s shape across every caller that needed a leaf value.

Common mistake

The failure mode that catches seniors: deleting a delegate that was a deliberate seam. Not every forwarding method is a middle man. A delegate that exists to be a stable abstraction boundary — the place you’ll later swap the backing object, insert a policy, add caching, or break a dependency — is doing real work even when its body is one line today. “Remove middle man” assumes the wrapper hides nothing; if it hides a boundary you’ll want to vary later, removing it re-couples every caller to the concrete type you were keeping them away from. Before you delete a one-line delegate, ask: is this pass-through noise, or a seam I’m about to need? The body looks identical; the intent is the whole difference.

Check yourself
Quiz

A reviewer flags Order.shippingCity() — a one-line method returning this.customer.address.city — as a middle man and says to delete it so callers use order.customer.address.city directly. When is the reviewer wrong?

Recap

A message chain (a.getB().getC().getD()) couples the caller to a whole object graph: every link becomes a fact the caller depends on, so the blast radius is the chain’s length. Hide delegate cures it by pushing the walk behind the nearest object, applying the Law of Demeter — talk only to immediate friends. But over-apply that and you get the mirror smell: a middle man, a class whose methods only forward, charging a pass-through tax with no behaviour of its own; remove middle man or tell-don’t-ask dissolves it. The two smells pull against each other, so the senior call is balance — hide the relationships likely to change, leave the public ones (list.length, fluent builders) alone. The failure mode to fear is deleting a delegate that was a deliberate seam: a one-line forward you’ll want as an abstraction boundary later. Same body, opposite intent — judging which you’re holding is the whole skill.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.