When is querying an object's state ("asking") the right design rather than a violation of Tell, Don't Ask? Give concrete categories.
answer
- ask + decide about the same object = smell
- display/serialize/report/test = fine ask
- CQS: command changes, query answers
- check-then-act race -> make it one call
- multi-object rule -> policy/domain service
basics
~20 sAsking 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 sTell, 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// 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
Say the smell is asking then deciding about the same object; give display/serialization/tests as fine.
List concrete legitimate categories and mention CQS - commands versus queries.
Lead with the check-then-act/atomicity argument and show outcome types, events or callbacks as ways to tell while still informing the caller.
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