Skip to main content
CleanCodeMastery

Message Chains: Asking a Friend, Who Asks a Cousin, Who Asks an Uncle

Learn the Message Chains code smell with a story about finding out if bread is available — through four people. When code reads a.getB().getC().getD(), the caller is coupled to a whole path. Learn the Law of Demeter and cure chains with Hide Delegate.

24 min read Updated June 11, 2026beginner
code-smellscouplersmessage-chainslaw-of-demeterhide-delegatetrain-wrecktypescriptcsharp

🍞 The bread enquiry that travelled through four people

It is Sunday morning in Gandhi Nagar. You want to know one small thing: is bread available at the corner shop?

But you do not ask the shopkeeper. That is not how things work in your lane. Instead, you ask your friend Amit, because Amit knows people. Amit does not know about bread, but his cousin Raju lives closer to the shop, so Amit phones Raju. Raju also does not know, but his uncle's house is right opposite the shop, so Raju shouts across the courtyard to Uncle. Uncle puts on his chappals, walks to the shop, and asks the shopkeeper. The shopkeeper says, "Yes, fresh bread came at seven."

Now the answer travels back. Uncle tells Raju. Raju phones Amit. Amit calls you. Twenty minutes, four people, one yes.

Figure 1: The Sunday morning bread enquiry — every hop adds delay and a new person to depend on

Now think about what you needed for this "system" to work. You needed Amit to be home. You needed Amit to remember that Raju is the right cousin. You needed Raju to know which uncle lives near the shop. If Raju shifts to Pune, the whole chain collapsesyour bread question breaks because of Raju's house move, and Raju does not even know you exist!

And here is the silliest part. Your question was about bread. But to get the answer, you had to know about Amit's cousin and the cousin's uncle — people who have nothing to do with bread. Next Sunday, when you want milk instead of bread, you will walk the same four-person route again. And the Sunday after that. Every small question pays the full toll.

There was always a better way, and deep down you knew it: ask Amit "is bread available?" and let Amit figure out his own family network. Or — radical thought — let the shop hang a board outside: Fresh bread available. One question, one answer, no map of anyone's relatives.

In code, the four-person route looks like one famous line:

const hasBread = me.friend().cousin().uncle().shopkeeper().hasBread();

A long train of calls, each one digging into the result of the previous one. This smell is called Message Chains. Programmers also call such a line a train wreck — count the dots, see the bogies of the train piled one after another.

🔍 What is this smell?

A Message Chain happens when code asks an object for another object, then asks that object for another object, then another — hopping across the object graph to reach the one value it actually wants:

order.getCustomer().getAddress().getCountry().getTaxRate()

Martin Fowler describes it in Refactoring as a client asking one object for another object, which the client then asks for yet another object, and so on. The client ends up coupled to the whole journey, not just the destination. Refactoring Guru's catalog lists it among the Couplers — the smells of classes tied together too tightly — alongside Feature Envy, Inappropriate Intimacy, and Middle Man.

Note carefully what the chain digs through. Each .get returns a different object of a different class: an Order gives a Customer, which gives an Address, which gives a Country. The caller travels through four classes to ask one question. Keep this "different objects" detail in mind — it is what separates the smell from innocent fluent APIs, which we will meet later.

💡

A reading trick: a message chain reads like directions, not like a question. "From the order, go to the customer, go to the address, go to the country, take the tax rate" — that is a route map. Healthy code reads like a question: "Order, what is your tax rate?" If your line sounds like directions to someone's house, it is probably a chain.

⚖️ The Law of Demeter: the rule the chain breaks

This smell has a famous law behind it. In the late 1980s, Karl Lieberherr and his colleagues on the Demeter project at Northeastern University wrote a style rule for object-oriented programs (OOPSLA 1988, and the 1989 paper "Assuring Good Style for Object-Oriented Programs"). It is called the Law of Demeter, or the Principle of Least Knowledge, and its motto is wonderfully simple:

"Only talk to your immediate friends. Don't talk to strangers."

A method's friends are:

  1. its own object (this) and its fields,
  2. its parameters,
  3. objects it creates itself.

The law says: call methods on your friends — but do not call methods on objects that your friends return to you from inside themselves. The friend's friend is a stranger.

Now look at the chain again. order is a friend (it is a parameter — fine). But order.getCustomer() hands you a stranger. Calling .getAddress() on that stranger breaks the law. The result of that is a stranger's stranger. Every extra dot walks deeper into houses you were never invited to.

The Sunday-morning rule: you may ask Amit, your direct friend, "Amit, is bread available?" — and let Amit find out however he wants. What you must not do is use Amit as a door to reach his cousin and his uncle yourself.

College corner: the formal object form of the law, for your notes: a method M of class C may only send messages to (1) C itself and its instance variables, (2) M's parameters, (3) objects M creates, and (4) global objects. The popular slogan "use only one dot" is a rough compile-time shortcut, and it over-fires: numbers.filter(isEven).map(square) has two dots and zero violations, because each call returns the same abstraction and never exposes anyone's internals. The precise question is never "how many dots?" but "whose internal structure am I traversing?" Researchers later formalised this as the difference between navigating an object graph (coupling to structure) and composing operations on one abstraction (coupling only to an interface).

Figure 2: The Law of Demeter as a memory map — friends you may call, strangers you must not

👀 How to spot it

The checklist:

  • A single expression has two or more hops through different objects: a.getB().getC().getD().
  • The same deep path is written in many places — five files all spell out order.customer.address.city.
  • The calling class names types it has no business knowing. A Renderer mentions Address and Country although it only wanted a tax rate.
  • Renaming or removing a middle getter causes compile errors in surprisingly distant files.
  • The line reads like a route ("from X, through Y, into Z") rather than a request for a result.
  • You see defensive null checks at every hop: if (o.getCustomer() != null && o.getCustomer().getAddress() != null …) — the famous staircase of fear.

Use this table to judge how serious a chain is:

What you seeSeverityWhy
One hop: order.customer()NoneTalking to a direct friend — completely normal
Two hops, written once, in stable codeMildTolerable; note it and move on
Two hops, repeated in many filesMediumOne structure change will break many files — wrap it
Three or more hops through different classesHighCaller knows the whole internal map; fix it
Long chains over structures that change oftenSevereEvery model change becomes a project-wide hunt
builder.setA().setB().build() — same object each timeNot a chain at allFluent API, deliberately designed (see the table below)

Two of those columns — how long the chain is and how often it is repeated — decide the urgency. Plot them and the decision makes itself:

Figure 3: Chain triage — length and repetition together decide what to fix first

Top-right — long and repeated — is where Hide Delegate pays for itself immediately. Bottom-left is normal collaboration; touching it would be busywork.

⚠️ Why it is a problem

1. It couples the caller to the entire path. The client now depends on the existence, names, and shapes of Customer, Address, and Country. Insert a new class between two links, or rename one getter, and every file that spelt out the path breaks — often files owned by other teams. One change, many broken places: the chain quietly manufactures Shotgun Surgery.

2. It leaks structure. For the chain to work, every intermediate class must keep exposing its internals as public getters. The object graph's private map becomes public property, frozen forever.

3. It hides intent. The reader must mentally walk the route to discover the goal was simply "the tax rate". Code should say what it wants, not how to crawl there.

4. It multiplies null-handling. Every hop can be null/missing. So callers wrap chains in pyramids of checks, or one day production throws the classic NullReferenceException from the middle of a train wreck — and the stack trace cannot even tell you which bogie was null.

Here is the chain drawn out — the bread enquiry as an object diagram:

Figure 4: The message chain — your code travels through four strangers to reach one answer

Every box in that line is a class your code now depends on. Five dependencies for one boolean. And remember: if any middle box changes shape, the red box on the left is the one that breaks.

How badly does it break? It depends on how many callers spelt out the route. The maths is unkind — the longer the chain, the more callers it tends to gather, and the wider the blast radius when one link changes:

Figure 5: Blast radius — callers broken when a single middle link changes shape

College corner: in coupling terms, a chain of length n gives the caller an efferent coupling to n classes instead of one, and every intermediate class gains afferent coupling from callers it never intended to serve. There is also a maintenance-research angle: studies of change propagation show the probability that a change ripples to a dependent file rises with each additional structural dependency — so a spelled-out path of four classes is, roughly, four chances to be broken per model change, multiplied across every file repeating the path. The chain converts one logical dependency ("I need the tax rate") into a bundle of structural dependencies ("I know your whole family tree"). That conversion is the entire smell.

⚠️

The cruellest property of message chains: the cost lands on the wrong person. Writing the chain is easy — the IDE autocompletes every hop, dot dot dot, done in ten seconds. The price is paid months later by whoever needs to change the model and discovers forty files spelling out the old route. Cheap to write, expensive to own.

💻 A real-life code example

Our school app prints a delivery label for sending report cards by post. Watch the renderer crawl through the graph:

class TaxTable { rateFor(state: string): number { return state === "MH" ? 0.18 : 0.12; } }
 
class Country {
  constructor(public name: string, public taxTable: TaxTable) {}
}
 
class Address {
  constructor(public city: string, public state: string, public country: Country) {}
}
 
class Customer {
  constructor(public name: string, public address: Address) {}
}
 
class Order {
  constructor(public customer: Customer, public amount: number) {}
}
 
// The smelly part — every method is a little train journey:
class LabelRenderer {
  shippingLine(order: Order): string {
    return `Ship to: ${order.customer.address.city}`;            // 3 hops
  }
 
  taxLine(order: Order): string {
    const rate = order.customer.address.country.taxTable        // 4 hops!
      .rateFor(order.customer.address.state);                   // and again!
    return `Tax: ${(order.amount * rate).toFixed(2)}`;
  }
}

Count the knowledge inside LabelRenderer. It is a printing class, yet it knows: orders hold customers; customers hold addresses; addresses hold city, state, and country; countries hold tax tables; tax tables answer by state. Six facts about other people's internals — to print two lines. Measure what this class actually contains:

Figure 6: What fills the LabelRenderer — mostly other classes' private maps, barely any printing

Now the disaster scenario. The model team decides addresses should support multiple shipping addresses: customer.addresses[] with a primary() method. A tiny, sensible model change. Result: every chain in every file that walked through .address breaks. The model team's change ripples into rendering, billing, SMS, exports... none of which care about the address list. That is the chain's bill arriving. Raju moved to Pune, and suddenly nobody in the lane can find out about bread.

The exact same wreck in Python, where dynamic typing means it does not even fail at compile time — it fails at 11 p.m. in production:

# Smelly: a route map, written in three different files
def tax_line(order):
    rate = order.customer.address.country.tax_table.rate_for(
        order.customer.address.state
    )
    return f"Tax: {order.amount * rate:.2f}"
 
# Healthy: one friend, one question
def tax_line(order):
    return f"Tax: {order.tax():.2f}"

🛠️ Cleaning it up, step by step

The cure is to stop giving directions and start asking questions. The main tool is Hide Delegate.

Step 1 — If a chain repeats, gather it first with Extract Method. Get the journey written in exactly one place:

class LabelRenderer {
  shippingLine(order: Order): string {
    return `Ship to: ${this.cityOf(order)}`;
  }
  private cityOf(order: Order): string {
    return order.customer.address.city;   // the only copy of the route
  }
}

Already better: the route exists once. But the renderer still knows it. Push it down.

Step 2 — Apply Hide Delegate, one link at a time. Ask: who is the renderer's direct friend? Order. So Order should answer the question, hiding its own internals:

class Customer {
  shippingCity(): string  { return this.address.city; }    // hop hidden inside Customer
  shippingState(): string { return this.address.state; }
  taxRate(): number {
    return this.address.country.taxTable.rateFor(this.address.state);
  }
}
 
class Order {
  shippingCity(): string { return this.customer.shippingCity(); }  // one hop, hidden
  tax(): number          { return this.amount * this.customer.taxRate(); }
}

Each class hides only its own next hop. Order knows it has a customer — fine, that is its direct friend. Customer knows it has an address — its direct friend. Nobody reaches two steps away.

Step 3 — The caller now asks one friend one question:

class LabelRenderer {
  shippingLine(order: Order): string {
    return `Ship to: ${order.shippingCity()}`;   // one friend, no strangers
  }
  taxLine(order: Order): string {
    return `Tax: ${order.tax().toFixed(2)}`;
  }
}

Compare the two conversations side by side — the chain versus the question:

Figure 7: Before, you interrogate the whole family; after, you ask one friend and the hops hide inside their owners

Run the disaster scenario again: customers get multiple addresses. What breaks? Only Customer.shippingCity() and friends — methods inside Customer, right next to the changed data. The renderer, billing, SMS, and exports do not change a single character. The change is absorbed where it happens.

Step 4 — Consider Move Method for the deepest logic. Notice the tax calculation truly belongs near the data: tax() on Order is also curing a Feature Envy — the renderer used to compute tax from other objects' data. Chains and envy often travel together; moving behaviour next to data cures both at once.

Step 5 — Stop before you overdo it. Every delegate you hide adds a small forwarding method to a middle class. Hide every hop in the whole project and your middle classes become walls of forwarders — that is the Middle Man smell, the exact opposite failure. Fowler is honest about this trade-off: Hide Delegate and Remove Middle Man are inverse refactorings, and the right point is in between. Wrap the chains that are long, repeated, or over unstable structures. Leave a short, local, stable two-hop alone.

The refactoring as a state machine — note the loop at the bottom, because this balance is adjusted again and again over a project's life:

Figure 8: From train wreck to clean — with the Middle Man ditch waiting if you hide too much

And the final structure. Each class exposes a question and keeps its map private:

Figure 9: After Hide Delegate — each class answers for its own next hop only

🧪 The same smell in C#

The classic null-decorated train wreck, in C#:

// Smelly: the controller knows the whole object map
public class InvoiceController
{
    public string TaxLabel(Order order)
    {
        var rate = order.Customer.Address.Country.TaxTable
                        .RateFor(order.Customer.Address.State);
        return $"GST: {order.Amount * rate:F2}";
    }
}

(C#'s ?. operator makes chains safer to run — order?.Customer?.Address?.City will not throw — but note carefully: it does not make them better designed. The coupling to the whole path remains; the null-conditional only silences the crash.)

After Hide Delegate:

public class Customer
{
    public decimal TaxRate() => _address.Country.TaxTable.RateFor(_address.State);
}
 
public class Order
{
    public decimal Tax() => Amount * _customer.TaxRate();
}
 
public class InvoiceController
{
    public string TaxLabel(Order order) => $"GST: {order.Tax():F2}";
}

One friend, one question, one place to change.

🏢 Where this smell hides in real projects

1. ORM entity navigation. Object-relational mappers make graphs effortlessly walkable: order.Customer.Address.Country.Name compiles, autocompletes, and also may fire three lazy-loading SQL queries you never noticed. Deep entity chains are a double smell in ORM code — design coupling plus hidden database trips (the N+1 query problem). Aggregate boundaries and intention-revealing query methods are the standard medicine.

2. Configuration and settings trees. config.getServer().getSecurity().getTls().getCertPath(). Every consumer learns the whole config layout, and reorganising the config file breaks half the codebase. A flat, typed options object per consumer is calmer.

3. Getter-only "struct" models. When classes expose structure (getAddress()) instead of behaviour (shippingCity()), they invite callers to navigate. The chain is the symptom; the structure-shaped API is the disease.

4. UI code digging into view models. screen.form().section(2).field("pin").value() — rearrange the form, break the code in every event handler.

5. Test setup chains. Tests that arrange data via order.getCustomer().getAddress().setCity("Pune") break en masse on every model change. Builders and factory helpers in tests are Hide Delegate's cousins.

6. Service locators. context.getRegistry().getService("billing").getClient() — chains through infrastructure are just as binding as chains through domain objects.

🤔 When it is okay to ignore

This section matters most for this smell, because one very common pattern looks identical and is perfectly healthy.

Fluent interfaces are NOT message chains. Look at these two lines:

// A: message chain (smell)
const rate = order.customer().address().country().taxRate();
 
// B: fluent builder (deliberate, healthy design)
const ticket = new TicketBuilder().forMovie("3 Idiots").seats(2).snack("popcorn").build();

They have the same dots. But they are opposites inside:

  • In A, every call returns a different object of a different class — you are travelling through a graph of strangers, deeper into other people's internals each step.
  • In B, every call returns the same builder object — you are giving instructions to one friend, step by step. The chain never leaves your immediate friend, so the Law of Demeter is not violated at all.

The same goes for LINQ and array pipelines: orders.Where(...).OrderBy(...).Take(10) keeps returning the same kind of thing (a sequence), by deliberate design. Ask the test question: "Am I configuring/transforming one thing, or am I travelling through several different things' insides?" Configuring one thing = fluent API, a feature. Travelling through strangers = message chain, a smell.

College corner: the design-theory difference is crisp. A fluent interface (the term comes from Eric Evans and Martin Fowler, 2005) is an API whose methods return the receiver — or another value of the same abstraction — precisely so that calls compose; the dots couple you only to one published interface. A message chain's dots couple you to a path of concrete structures. Formally: in a fluent pipeline, the static type at every dot is the same (or a deliberate supertype); in a message chain, the type changes at every dot, and each new type is a new dependency edge in your coupling graph. That is why stream().filter().map().collect() survives any internal refactoring of the collection, while school.dept().head().phone() dies if a single class on the path changes. When in doubt, write down the type at each dot — if the list reads like a tour of your domain model, it is a chain.

SituationVerdictWhy
Fluent builder: b.setX().setY().build()Not a smellEvery call returns the same object; one friend, many instructions
LINQ / stream pipeline: xs.filter(...).map(...)Not a smellEach step returns the same abstraction (a sequence), designed for chaining
Two-hop chain, written once, stable modelTolerableCost of wrapping may exceed the benefit; note it and move on
Internal chain inside the owning moduleOften fineA class navigating its own composed parts knows them legitimately
3+ hops through domain objects, repeated in many filesFix itOne model change will break them all — Hide Delegate pays for itself
Chains over a model that is still changing every sprintFix it firstUnstable structure + spelled-out routes = constant breakage
ℹ️

And remember the opposite ditch: hiding every delegate adds forwarding methods to middle classes, and too many of those creates the Middle Man smell. Demeter pushes you towards delegation; Middle Man warns you when delegation becomes the only thing a class does. Good design walks between the two ditches — that balance is the whole subject of the next post.

💊 Which refactorings cure it

RefactoringWhen to use itWhat it does
Hide DelegateThe primary cure for chainsGives the first object a method that fetches the deep value; hops hide inside their owners
Extract MethodThe same chain appears in many placesGathers the route into one named method before pushing it down
Move MethodThe chained code is really computing with end-of-chain dataMoves the whole behaviour next to the data; the chain vanishes entirely
Remove Middle ManYou overshot and made forwarding-only classesThe inverse correction — let callers talk a step closer again

📦 Quick revision box

+=================================================================+
|                 MESSAGE CHAINS — QUICK REVISION                  |
+=================================================================+
| STORY    : You ask Amit, who asks Raju, who asks uncle, who      |
|            asks the shopkeeper — 20 minutes to learn if          |
|            bread is available.                                   |
|                                                                  |
| SMELL    : a.getB().getC().getD() — a train wreck of hops        |
|            through DIFFERENT objects to dig out one value.       |
|                                                                  |
| LAW      : Law of Demeter — "only talk to your immediate         |
|            friends." Friends: this, own fields, parameters,      |
|            objects you create. A friend's friend = stranger.     |
|                                                                  |
| COSTS    : coupled to the whole path, leaked structure,          |
|            hidden intent, null-check pyramids                    |
|                                                                  |
| CURE     : Extract Method (gather the route)                     |
|            -> Hide Delegate (each class hides its own hop)       |
|            -> Move Method (put behaviour beside its data)        |
|                                                                  |
| NOT A    : fluent builders & LINQ — every call returns the       |
| CHAIN    : SAME object/abstraction. Configuring one friend       |
|            is fine; touring strangers is the smell.              |
|                                                                  |
| BALANCE  : hide too many delegates and you create the            |
|            MIDDLE MAN smell. Wrap long/repeated/unstable         |
|            chains; leave short stable ones alone.                |
+=================================================================+

✏️ Practice exercise

Here is a hospital app with proud train wrecks:

class Pharmacy   { stockOf(med: string): number { return 42; } }
class Ward       { constructor(public pharmacy: Pharmacy, public floor: number) {} }
class Department { constructor(public ward: Ward, public head: string) {} }
class Hospital   { constructor(public department: Department) {} }
 
class ReceptionDesk {
  canDispense(h: Hospital, med: string): boolean {
    return h.department.ward.pharmacy.stockOf(med) > 0;          // chain 1
  }
  directions(h: Hospital): string {
    return `Go to floor ${h.department.ward.floor}`;              // chain 2
  }
  noticeFor(h: Hospital): string {
    return `Dept head: ${h.department.head}`;                     // chain 3
  }
}

Your tasks:

  1. Measure the wrecks. For each of the three methods, count the hops and list every class ReceptionDesk is coupled to. Which method is the worst train?
  2. Apply the Law of Demeter. In canDispense, name the method's friends using the formal statement from the College corner. At exactly which dot does the code start talking to a stranger?
  3. Refactor with Hide Delegate. Add hasStock(med) to Hospital, delegating downwards level by level (HospitalDepartmentWardPharmacy), so that ReceptionDesk calls just h.hasStock(med). Make every field private as you go.
  4. Feel the trade-off. After step 3, look at Department and Ward. How many of their methods are now pure forwarding? Write one sentence on whether they are drifting towards Middle Man, and what you would do if three more chains demanded hiding.
  5. Sort the impostors. Which of these are real message chains, and which are healthy fluent designs? Write down the type at each dot, then give a one-line verdict:
    • patient.history().lastVisit().doctor().phone()
    • new Query().from("patients").where("age > 60").limit(10)
    • report.sections.filter(s => s.urgent).map(s => s.title)
    • app.config().database().pool().maxSize()
  6. Bonus. The hospital adds multiple departments (h.departments[]). With your refactored code, which class absorbs the change? How many files would have changed with the original code?

If you answered question 5 with "same object/abstraction returning = fluent; different strangers = chain", the Law of Demeter is now your friend too. Bahut badiya!

Frequently asked questions

What is the Message Chains code smell?
It is a long train of calls like order.getCustomer().getAddress().getCity() — the code hops through several objects to dig out one value. The caller becomes coupled to every class on the path, so a change to any middle link breaks it. Programmers also call this a 'train wreck' because of all the dots in a row.
How do message chains violate the Law of Demeter?
The Law of Demeter says a method should talk only to its immediate friends: itself, its fields, its parameters, and objects it creates. A chain calls a method on the RESULT of a method, then again on that result — each extra dot talks to a stranger. The fix is to ask your direct friend a question and let it do any further navigation.
Are fluent builders and LINQ queries message chains?
No! In a fluent interface like builder.setName().setAge().build() or orders.Where(...).OrderBy(...), every call returns the same object or the same kind of object, by design. You are configuring one thing, not travelling through a graph of different strangers. Method chaining on one abstraction is a feature; navigating through many objects' internals is the smell.
Which refactoring fixes Message Chains?
Hide Delegate is the main cure: give the first object a method that fetches the deep value, hiding the hops inside. Extract Method gathers a repeated chain into one place first, and Move Method can shift the whole calculation next to the data so the chain disappears entirely.
Can fixing message chains create another smell?
Yes. Every hidden hop adds a forwarding method to an intermediate class. Hide every delegate everywhere and those classes become Middle Men — classes that only forward. Wrap the chains that are long, repeated, or built over changing structures; leave short, stable, local ones alone.

Further reading

Related Lessons