skip to content

How does Tell, Don't Ask differ from the Law of Demeter, and why does fixing a "train wreck" like `a.getB().getC().doThing()` by adding delegating methods sometimes make the design worse?

level: seniorimportance: should knowfreq 30%

answer

  1. Demeter = reach/structure; TDA = responsibility
  2. train wreck a.getB().getC()
  3. delegation without moving decision = middle man
  4. ask 'what did the caller want to do?'
  5. builders/streams/DTOs exempt

basics

~20 s

The Law of Demeter limits how far you may navigate - only talk to immediate collaborators. Tell, Don't Ask says who should make a decision. Blindly adding pass-through methods satisfies Demeter's letter while still asking, and bloats the middle object.

solid answer

~50 s

The Law of Demeter ("principle of least knowledge") is structural: a method may call methods on itself, its parameters, objects it creates, and its own fields - not on objects returned by those calls. It forbids chained navigation like `order.getCustomer().getAddress().getZip()` because the caller then depends on the whole object graph's shape. Tell, Don't Ask is about responsibility placement: the decision should live where the data lives. They usually flag the same code, but from different angles - Demeter sees coupling to structure, Tell sees a misplaced decision. The failure mode of a mechanical Demeter fix is the "Demeter middleman": adding `order.getCustomerZip()` removes the dots but keeps the caller deciding, and every such wrapper grows the middle class into a forwarding facade with no behavior of its own. The better fix asks what the caller wanted to *do* - `order.shipTo(carrier)` or `order.calculateTax(rates)` - so the chain disappears because the decision moved, not because it was wrapped. Fluent builders, streams and pure value chains are deliberate exceptions.

code

pseudocode · 8 lines
pseudocode
// Train wreck: violates Demeter and asks
if (order.getCustomer().getAddress().getZip().startsWith("9")) rate = WEST;

// Mechanical 'fix': Demeter-legal, still asking, Order becomes a middleman
if (order.getCustomerZip().startsWith("9")) rate = WEST;

// Real fix: the decision moves to the owner of the data
Money shipping = order.shippingCost(rateTable);

go deeper

for a junior

State both definitions and identify a train wreck; say the fix is usually to move the operation.

for a middle

Contrast reach versus responsibility, and show the delegating-getter fix and why it is only cosmetic.

for a senior

Name the middleman smell, show a case violating each principle alone, and list exceptions (builders, streams, DTOs, config trees).

for a principal

Discuss how the pattern signals a missing domain concept, and when boundary data traversal is legitimately structural rather than behavioral.

## Definitions **Law of Demeter (LoD)**, formulated at Northeastern University for object-oriented style, also called the *principle of least knowledge*. In its common object form, a method `m` of object `O` may only invoke methods of: 1. `O` itself, 2. `m`'s parameters, 3. objects `m` creates, 4. `O`'s direct component objects (fields), 5. (often added) globals/singletons accessible to `O`. What it forbids is calling methods on the *return value* of another call - the "one dot" heuristic (imprecise, but memorable). `a.getB().getC().doThing()` is a **train wreck**. **Tell, Don't Ask (TDA)**: if a decision depends on an object's data, that object should make it. TDA is about *responsibility*; LoD is about *reach*. ## Why they overlap A train wreck is usually a symptom of ask-style code: the caller navigates the graph precisely because it wants data to decide with. So the same line trips both rules. But they are not equivalent: - LoD violation without TDA violation: `config.getSection("db").getString("url")` - deep navigation, but no decision stolen from anyone. Coupling to structure is the only cost. - TDA violation without LoD violation: `if (order.getStatus() == DRAFT) order.setStatus(SUBMITTED)` - a single dot each, perfectly Demeter-legal, and still the wrong owner for the decision. ## The middleman failure mode Mechanically satisfying LoD by delegation: ``` class Order { String getCustomerZip() { return customer.getAddress().getZip(); } } // caller: if (order.getCustomerZip().startsWith("9")) applyWestCoastRate(); ``` The dots are gone; the design is not better: - The **decision still lives in the caller** - TDA is untouched. - `Order` grows one forwarding method per caller need - the *middle man* smell; its API becomes a projection of everybody's queries. - Coupling is now hidden rather than removed: `Order` still depends on Address's shape, and every caller depends on Order's swelling surface. - Refactoring tools and reviewers stop seeing the chain, so the real problem is camouflaged. ## The better move Ask *what is the caller trying to accomplish?* and name that: ``` Money tax = order.calculateTax(rateTable); // Order asks its own customer/address order.shipWith(carrier); // Order tells its collaborators ``` Or introduce the missing concept: if many callers need zip-based rules, the concept is a `ShippingZone` value object that owns them. Now the chain is gone because nobody needs to navigate, and the middle object gained *behavior*, not forwarding. Other legitimate resolutions: - **Pass the collaborator in** (dependency injection / parameter object) instead of navigating to it. - **Move the method** to the class at the end of the chain (Move Method), the classic feature-envy fix. - **Publish an event or callback** so the far object reacts instead of being fetched. ## Deliberate exceptions - **Fluent interfaces / builders**: `builder.a().b().c()` returns the same object; there is no graph navigation. - **Stream/collection pipelines and immutable value chains**: `text.trim().toLowerCase().split(",")` - pure transformations over values with no hidden state to protect. - **Data structures / DTOs / config trees**: these exist to be traversed; LoD's rationale (protecting behavior and invariants) does not apply. - **Test builders and assertions**, where readability outranks coupling. ## Practical guidance 1. Treat a train wreck as a *question*, not a defect: what decision is being assembled here? 2. If the answer names a domain operation, move the operation (TDA fix). 3. If there is no decision - just data plumbing at a boundary - a projection method or DTO is fine, and adding behavior would be wrong. 4. Only add a delegating accessor when it expresses a genuine concept of the middle object (`order.shippingZone()`), never one per caller. 5. Watch cohesion metrics/reviews for classes that are all one-line forwarders - that is the middleman accumulating.

  • Give a case that violates the Law of Demeter but not Tell, Don't Ask, and vice versa.
    LoD only: `config.getSection("db").getString("url")` - deep traversal of a data tree, but no decision was taken from an object that owned it. TDA only: `if (order.getStatus() == DRAFT) order.setStatus(SUBMITTED)` - single dots throughout, fully Demeter-legal, yet the submission rule belongs on Order.
  • Do fluent APIs and stream pipelines violate the Law of Demeter?
    Formally the chain returns objects whose methods you then call, but the rationale does not apply: builders return the same object, and stream/value chains are pure transformations with no encapsulated state or invariant to protect. Applying the rule there would hurt readability and buy nothing.

Demeter says: don't walk into your colleague's manager's office to grab a file. Adding a pass-through method is your colleague fetching the file for you every time - the rule is satisfied, but they have become a courier, and you are still the one making the decision the manager should have made.

saying these in an interview costs you the question

  • Stating the two principles are the same rule with different names
  • Applying the 'one dot' heuristic mechanically to builders, streams and string/value chains
  • Adding a delegating getter per caller and calling the design fixed
  • Assuming Demeter forbids method chaining in general rather than navigation through a foreign object graph
  • Refactoring a config/DTO traversal into behavior on a data structure that has no invariants

context