Tell, Don't Ask and the Law of Demeter

Apply Tell, Don't Ask and the Law of Demeter to keep object rules near their data, reduce fragile navigation, and see when a direct query is clearer in context.

published: reading time: 12 min read author: GeekWorkBench
Quick Summary

Tell, Don't Ask helps place decisions beside the state and invariants they govern, while the Law of Demeter limits how much a method depends on objects beyond its direct collaborators. Through checkout examples, Java snippets, and production failure scenarios, the article shows when a command or narrow interface clarifies ownership and when a query or method chain remains appropriate. Use the trade-offs and review questions to reduce accidental coupling without obscuring the workflow.

Tell, Don’t Ask and the Law of Demeter

Introduction

A service that reads several fields, makes a business decision, then writes a result back is doing work that may belong to one of the objects it inspected. A method chain such as order.getCustomer().getAccount().getPoints() creates a different problem: the caller knows the shape of an object graph and can break when that shape changes.

Tell, Don’t Ask and the Law of Demeter are related design heuristics for these situations. The first asks where a decision belongs. The second asks how much of an object graph a method needs to know. Neither is a ban on getters, queries, or readable code. For an intermediate developer, the useful skill is spotting when these patterns reveal misplaced responsibility or avoidable coupling, then checking whether moving the behavior actually improves the design.

Two Heuristics, Different Questions

Tell, Don’t Ask

Tell, Don’t Ask suggests asking an object to perform behavior instead of extracting its state so the caller can perform that behavior on its behalf. When an object owns an invariant, keeping the decision near that state makes invalid updates harder to introduce.

For example, if application code checks an order’s fields to decide whether it can be cancelled, then separately changes its status, the rule can drift across callers. A method such as order.cancel() gives the order one place to enforce the transition. This does not mean every method should mutate state. A query like order.total() is often the right interface when the caller needs a value to display, compare, or pass to another system.

The Law of Demeter

The Law of Demeter (LoD), often summarized as “talk only to your immediate friends,” limits how much a method depends on objects behind its direct collaborators. In its common object-oriented form, a method should work with itself, its parameters, objects it creates, and its direct fields or components. It should avoid reaching through one collaborator to control another object it does not own.

The familiar warning sign is a chain like checkout.getOrder().getCustomer().getAddress().getPostalCode(). The deeper issue is that Checkout now depends on the internal route from a checkout to an address. If the model changes, unrelated callers may all need edits. A short fluent chain on one value, such as money.add(tax).round(), does not automatically have the same coupling problem; understand the returned objects and their contracts before judging by punctuation alone.

Keeping a Rule at the Object Boundary

Suppose a checkout application needs to apply a loyalty promotion. A procedural version can pull customer data into the service and set the discount itself:

// Java: the service owns a rule that depends on Order and Customer internals.
if (order.getCustomer().getPoints() >= 500) {
    order.setDiscount(order.getSubtotal().multiply(0.10));
}

This caller has to know how points qualify, how to compute a discount, and how to update the order. Another caller can copy the logic and forget one of those details. A small change to the customer model can also ripple into every place that traverses it.

Keep the policy in a collaborator and give that collaborator a clear operation on the order:

public final class LoyaltyPromotion {
    public void applyTo(Order order) {
        if (order.qualifiesForLoyaltyPromotion()) {
            order.applyDiscount(order.subtotal().multiply(0.10));
        }
    }
}

public final class CheckoutService {
    private final LoyaltyPromotion loyaltyPromotion;

    public CheckoutService(LoyaltyPromotion loyaltyPromotion) {
        this.loyaltyPromotion = loyaltyPromotion;
    }

    public void finish(Order order) {
        loyaltyPromotion.applyTo(order);
        order.submit();
    }
}

CheckoutService tells the promotion to apply itself and tells the order to submit. The eligibility rule belongs behind Order’s domain interface, where it can consult its direct customer collaborator without exposing a customer-points path to every caller. Order.applyDiscount can enforce its own constraints, such as rejecting a discount after submission. The promotion remains a separate policy because it may vary by campaign or be tested independently.

This design is useful only if the ownership matches the domain. If qualifiesForLoyaltyPromotion() becomes a pass-through that just returns customer.getPoints() >= 500, the extra method may hide rather than clarify. Give the operation a meaningful rule or use a value query where that is genuinely all the caller needs.

flowchart LR
    A[Checkout Service] -->|applyTo order| B[Loyalty Promotion]
    B -->|ask eligibility| C[Order domain interface]
    C -->|check direct collaborator| D[Customer]
    B -->|apply discount| C
    A -->|submit| C

When to Use

Use these heuristics when a caller repeatedly reaches through object relationships to make a domain decision or mutate another object’s state. They are especially helpful when the decision must preserve an invariant, when several callers have duplicated the same rule, or when internal model changes keep forcing edits in distant modules.

A practical review question is: “Who owns the facts this rule depends on, and who should be responsible for keeping the result valid?” If one object owns both the relevant state and its invariant, a behavior method there may be clearer. If a distinct policy owns a rule that varies independently, let that policy collaborate through a small, stable interface.

When NOT to Use

Do not move every conditional into a domain object just to satisfy a slogan. A report builder may need read-only values from several sources to format a report; a DTO is designed to expose data across a boundary; and a controller may translate an HTTP request into a domain command. In those cases, a simple query or mapping step can be the clearest choice.

Avoid wrapping every getter with a method that merely forwards the same value. That creates a façade without reducing meaningful coupling. Also avoid turning a stable, useful abstraction into dozens of tiny commands that make a simple workflow harder to follow. Prefer a direct query when the caller is supposed to make the decision, and prefer a command when the object should protect its own rule.

A report mapper, for example, can read values to build a view model without moving domain behavior into the mapper:

public OrderSummary toSummary(Order order) {
    return new OrderSummary(order.id(), order.status(), order.total());
}

Here the mapper translates data for presentation; it does not decide whether the order may be cancelled or change the order’s state. A direct query is the right tool for this job.

Production Failure Scenarios

Duplicated decisions drift

Two endpoints each check a status before allowing cancellation. A later change adds a payment-settlement restriction to one endpoint but not the other. Users see different behavior depending on the route. Moving the transition rule into order.cancel() gives both call sites the same guard and a shared failure result.

Internal navigation leaks across modules

A notification worker navigates through shipment.getOrder().getCustomer().getPreferences() to find a contact address. The order model later replaces its customer reference with an account identifier, breaking the worker even though notification behavior has not changed. A direct operation such as shipment.notificationRecipient() can keep that lookup behind the shipment’s model boundary, if shipment is the right owner.

A command silently bypasses an invariant

A service asks for order.getStatus(), decides an order is editable, then calls setStatus() and setItems(). A concurrent update or a new business rule can make this sequence invalid. An atomic domain operation can check the current state and perform the transition together. In concurrent systems, the persistence layer still needs an appropriate transaction or optimistic-lock check; object methods alone do not eliminate races across processes.

Trade-Off Table

Choice Helps with Cost or risk Good fit
Tell an object to perform a domain operation Keeps decisions and invariants near owned state Can create an anemic pass-through API if behavior has no real owner A state transition or rule must remain consistent across callers
Query a value and decide in the caller Makes reporting and orchestration explicit Caller may become coupled to representation or duplicate policy The caller owns the decision or is preparing a view/integration payload
Traverse a collaborator chain Quick access to nested information Couples caller to object graph and makes model changes ripple A one-off read in a narrow adapter where those relationships are intentionally public
Add a façade or domain operation Hides a volatile lookup behind a stable contract Adds indirection and maintenance surface Many callers need the same meaningful operation or the graph is likely to change

Observability Checklist

These principles do not replace runtime monitoring. They can make failures easier to locate when the code reports useful domain outcomes and keeps transitions in one place.

  • Record the operation and outcome for important transitions, such as order.cancelled or order.cancellation_rejected.
  • Include a correlation or order identifier, but avoid logging personal fields such as full addresses or loyalty balances.
  • Track counts of rejected commands and unexpected state transitions so rule changes surface in production.
  • Preserve the reason for a rejection in a structured result or domain exception that the boundary layer can map to a response.
  • Keep logging at the application or infrastructure boundary; domain objects should not need a logging framework just to enforce invariants.

Security and Compliance Notes

An object method can reduce the number of places that can mutate sensitive state, but encapsulation is not an authorization boundary by itself. A public order.cancel() still needs the application layer to verify that the authenticated actor may cancel that order. Enforce access control at a trusted boundary and ensure the domain operation receives only the authorized command or context it needs.

Avoid returning mutable collections or sensitive nested objects simply to help callers make decisions. Expose narrow queries or operations, validate untrusted input at system boundaries, and keep audit events for regulated changes. Do not log credentials, payment details, personal data, or raw access tokens while tracing a behavior path. For persistence-backed objects, use transaction and authorization checks appropriate to the deployment; a neat object graph cannot prevent a direct database write or a race from another process.

Common Pitfalls / Anti-Patterns

  • “No getters” as a rule: Read access is often legitimate. Focus on misplaced decisions and mutation paths, not method names.
  • Demeter counting by dots: A dotted expression can be harmless, while one method call can expose too much. Inspect dependency and ownership, not just syntax.
  • Anemic wrappers: A method like order.customerPoints() can preserve the same dependency under a different name if it contains no domain meaning.
  • Overgrown domain objects: Do not put application orchestration, persistence, formatting, and every business policy into one entity. Delegate independent policies through clear contracts.
  • Confusing DTOs with domain entities: A read model may intentionally expose data. Apply encapsulation rules where behavior and invariants live, not blindly to transport shapes.
  • Ignoring transaction boundaries: Moving a rule into an object does not make several database writes atomic or solve concurrent updates.

Quick Recap Checklist

  • Does this caller inspect state and then change the same object’s state on its behalf?
  • Is the decision based on an invariant owned by one object?
  • Is the method traversing collaborators to reach a detail that could stay behind a stable interface?
  • Would a new command represent real behavior, or only rename a getter chain?
  • Is a query clearer because the caller owns reporting, mapping, or orchestration?
  • Are authorization, concurrency, and persistence guarantees enforced at their actual boundaries?

Interview Questions

1. What does Tell, Don't Ask mean in object-oriented design?

It is a heuristic to put decisions and state changes near the objects that own the relevant data and invariants. Instead of extracting several values, making a rule elsewhere, and writing the result back, ask the responsible object or policy to perform a meaningful operation. It does not prohibit queries used for display, reporting, or decisions owned by another layer.

2. How does the Law of Demeter differ from Tell, Don't Ask?

Tell, Don't Ask focuses on where behavior and decisions belong. The Law of Demeter focuses on limiting how much a method knows about collaborators beyond its immediate neighborhood. They often point to the same design improvement, but neither is a mechanical ban on getters or all method chains.

3. When is it reasonable to ignore a method chain or use a getter?

When the caller legitimately owns the read or decision, as in a report mapper, API adapter, or immutable value pipeline, a query may be the simplest interface. Check whether the caller depends on a volatile internal structure or duplicates a rule. If neither is true, adding a forwarding method may make the design harder to understand.

Further Reading

Conclusion

Summary

Tell, Don’t Ask helps place rules beside the state and invariants they govern. The Law of Demeter helps keep callers from depending on the internal path through a collaborator graph. Use both as questions during design reviews, then keep the simpler query or chain when it expresses a real responsibility cleanly. A good design minimizes accidental knowledge while keeping the actual workflow easy to follow.

Category

Related Posts

GRASP: Assigning Object Responsibilities

Learn all nine GRASP patterns through a Java order workflow, with code, trade-offs, failure cases, and design review questions for intermediate developers.

#object-oriented-design #grasp #responsibility-assignment

SOLID Principles in Practice

Apply all five SOLID principles to a small checkout design, see where they help, and learn when extra interfaces or indirection make code harder to change.

#object-oriented-design #solid #design-principles

Behavioral Patterns: Organize Collaboration

Compare all eleven GoF behavioral patterns by the change they isolate, the coupling they reduce, and the runtime costs they add to a system in real code.

#design-patterns #object-oriented-design #behavioral-patterns