skip to content

When is querying an object's state ("asking") the right design rather than a violation of Tell, Don't Ask? Give concrete categories.

level: middleimportance: should knowfreq 34%

answer

  1. ask + decide about the same object = smell
  2. display/serialize/report/test = fine ask
  3. CQS: command changes, query answers
  4. check-then-act race -> make it one call
  5. multi-object rule -> policy/domain service

basics

~20 s

Asking is fine when you only need the value, not a decision about that object: displaying or serializing data, reporting and analytics, test assertions, and pure value objects. The smell is asking and then deciding on the object's behalf.

solid answer

~50 s

Tell, Don't Ask targets one shape - read another object's state, then make a decision about that same object. Legitimate asking includes: (1) presentation and serialization, where a UI, API mapper or event payload needs values; (2) queries and reporting, including CQRS read models that are data by design; (3) value objects and pure functions, where returning a computed value is the behavior (`money.plus(x)` returns money); (4) test assertions and diagnostics; (5) collection/query APIs, where the whole contract is answering questions; (6) decisions needing several objects' data, which belong to a coordinating policy or domain service rather than to any single owner. Command-Query Separation helps: commands are told and change state; queries are asked and are side-effect free. Violations happen when a query result is used to reimplement the owner's rules, or when a check-then-act sequence across a boundary creates a race window that only the owner can close atomically.

code

pseudocode · 6 lines
pseudocode
// Racy ask-then-act: the answer can change between the two calls
if (inventory.hasStock(sku, n)) inventory.reserve(sku, n);

// Tell, with an outcome the caller may branch on safely
result = inventory.reserve(sku, n);   // atomic inside the owner
switch (result) { case Reserved: ...; case OutOfStock: ...; }

go deeper

for a junior

Say the smell is asking then deciding about the same object; give display/serialization/tests as fine.

for a middle

List concrete legitimate categories and mention CQS - commands versus queries.

for a senior

Lead with the check-then-act/atomicity argument and show outcome types, events or callbacks as ways to tell while still informing the caller.

for a principal

Discuss where the rule stops applying (process and layer boundaries, read models in CQRS) and how the write-side/read-side split institutionalizes the answer.

## The precise target of the principle The smell is not `get`. It is: **`x.query()` -> caller branches -> caller mutates or acts on `x`'s behalf.** If the query result feeds a decision that `x` should have made, that is the violation. If it feeds display, transport, or a decision that is genuinely the caller's own, it is not. ## Categories where asking is correct **1. Presentation and serialization.** Views, templates, API DTO mappers, log lines, event payloads. These are boundary translations; embedding them in the domain object would couple the domain to wire formats. Reduce the exposure with a purpose-shaped projection (`order.summary()`) rather than field-by-field getters when convenient. **2. Reporting, analytics, search.** Aggregation over many objects has no single owner. Read models (in CQRS - Command Query Responsibility Segregation, where the write path and read path use different models) are intentionally anemic data structures. **3. Value objects and pure computations.** For an immutable value (money, date range, coordinate), returning a value *is* telling: `range.overlaps(other)` and `money.plus(x)` are queries with the decision inside. Constructing a new value rather than mutating avoids the invariant problem entirely. **4. Tests and diagnostics.** State-based assertions and debug/observability output need readable state. Prefer asserting through domain queries (`order.isSubmitted()`) over raw fields, so the test does not lock the representation. **5. APIs whose entire job is answering.** Repositories, collections, caches, feature-flag lookups, configuration. `list.isEmpty()` is fine; the caller's decision is about *its own* control flow, not about the list's internals. **6. Decisions requiring several objects.** No single object owns the data, so no move makes the envy vanish. Put the rule in a domain service, policy, or a new value object that models the combined concept - and *tell* that collaborator to decide. **7. Layer/process boundaries.** Across an HTTP or messaging boundary you exchange data, not behavior. The principle applies *within* each side; the payload itself is data. ## Command-Query Separation (CQS) as the companion rule Bertrand Meyer's CQS: every method is either a **command** (changes state, returns nothing meaningful) or a **query** (returns a value, changes nothing observable). Tell, Don't Ask tells you *who decides*; CQS tells you *what a method may do*. Together: tell commands to the owner; ask side-effect-free queries freely. Note the tension - some correct designs must break CQS deliberately (`stack.pop()`, `queue.poll()`, atomic `compareAndSet`, `getAndIncrement`) precisely because splitting the query from the command opens a race. ## The check-then-act trap ``` if (!inventory.hasStock(sku, n)) return; // ask inventory.reserve(sku, n); // act - may fail: someone reserved in between ``` Between the query and the command another thread/request/process may change the answer. Only the owner can make the decision and the effect atomic: `inventory.reserve(sku, n)` returning `Reserved | OutOfStock`. This is the strongest practical argument for the principle and applies to threads, database rows, and remote services alike. ## Returning results without breaking the principle Telling does not mean losing information. Idioms that keep the decision inside while giving the caller an answer: - **Result/Either/outcome types**: `withdraw(amount) -> Ok | InsufficientFunds`. - **Domain events**: the object returns what happened; the caller reacts. - **Callbacks / double dispatch**: `payment.settle(onSuccess, onDecline)` - the object decides which branch runs. - **New immutable values**: `account.withdraw(x)` returns a new account state. Callers still branch, but on an outcome the owner produced - not on internals they re-interpreted. ## Edge cases - **Over-application** yields wrapper methods that only forward, and objects bloated with caller-specific concerns. - **Boolean-returning queries used to guard an owner-rule** (`if (o.canSubmit()) o.submit()`) are borderline: acceptable for UX (enabling a button) but the command must still enforce the rule itself, or the guarantee is only advisory.

  • How does Command-Query Separation interact with Tell, Don't Ask, and when should CQS be broken?
    CQS constrains each method (command mutates, query answers, never both); Tell, Don't Ask constrains where decisions live. They reinforce each other, but CQS is deliberately broken where atomicity requires it - pop(), poll(), compareAndSet(), getAndIncrement() - because splitting query from command re-creates the check-then-act race the principle warns about.
  • Is `if (order.canSubmit()) order.submit()` a violation?
    Only partly. Exposing a query for UX (enabling a button, pre-validating a form) is reasonable, but submit() must re-check the rule itself; otherwise the invariant depends on every caller calling the guard. The query is a convenience, never the enforcement point.

Reading a thermostat's display to write it in a logbook is asking, and it is fine. Reading the display, computing what the temperature should be, and manually flipping the heater relay is asking-and-deciding - if the thermostat's rules change, your logbook habit is safe but your relay flipping is now wrong.

saying these in an interview costs you the question

  • Treating every getter call as a violation, including presentation, serialization and test code
  • Ignoring the atomicity argument: replacing check-then-act with a single command is often about correctness, not style
  • Believing Tell, Don't Ask forbids returning results, instead of using Result/outcome types or events
  • Pushing multi-object rules onto one entity because 'behavior must live on entities'
  • Assuming CQS must never be violated, when pop()/compareAndSet() exist precisely to avoid a race

context