---
title: "Tell, Don't Ask"
source: 'https://academia.sh/en/courses/design-principles/tell-dont-ask'
course: 'Design Principles'
language: en
updated: '2026-08-23T07:01:17+00:00'
license: 'CC BY-SA 4.0'
---

# Tell, Don't Ask

Moving behavior into the data: comparing a design where four clients each build the same dispatch rule with a design that tells the object the decision, counting the number of files that know the rule's fields, measuring the clients' divergent result set, and finding the number of files edited when the rule changes.

In the Demeter lesson, chains got shorter, but the intermediate objects still returned
values: `provinceDistrict()` hands back a string, and the caller makes the decision. This
hides another form of the same problem. Every client that takes fields from an object and
decides by looking at those fields carries a copy of the rule inside itself. As the number
of copies grows, the rule no longer has a single definition.

**Tell, don't ask** targets this repetition: the object that owns the data is told what to
do, instead of having its data taken so the decision can be made outside it. Its measure is
two numbers — how many files know the fields the rule depends on, and how many files repeat
the same rule.

## The Design That Writes the Rule Four Times

The dispatch rule consists of three conditions: the shipment must be in the warehouse, it
must be paid, and its weight must not exceed the limit. Four clients each build this rule
separately.

```sh
mkdir -p ask tell
```

```js
// ask/record.mjs — shipments awaiting dispatch
export const RECORDS = [
  { code: "GN-1", state: "warehouse", paid: true, weight: 12 },
  { code: "GN-2", state: "warehouse", paid: true, weight: 45 },
  { code: "GN-3", state: "in_transit", paid: true, weight: 8 },
  { code: "GN-4", state: "warehouse", paid: false, weight: 5 },
  { code: "GN-5", state: "warehouse", paid: true, weight: 25 },
];
```

```js
// ask/web.mjs — the HTTP endpoint builds the rule itself
export function dispatch(record) {
  if (record.state === "warehouse" && record.paid && record.weight <= 30) {
    return { httpStatus: 200, code: record.code };
  }
  return { httpStatus: 409, code: record.code };
}
```

```js
// ask/batch.mjs — the batch job rewrites the same rule
export const selected = (records) =>
  records
    .filter((r) => r.state === "warehouse" && r.paid && r.weight <= 30)
    .map((r) => r.code);
```

```js
// ask/courier.mjs — the courier app never wrote the weight condition
export const list = (records) =>
  records.filter((r) => r.state === "warehouse" && r.paid).map((r) => r.code);
```

```js
// ask/scheduled.mjs — the scheduled job writes the rule a third time
export function scan(records) {
  const out = [];
  for (const r of records) {
    if (r.state === "warehouse" && r.paid && r.weight <= 30) out.push(r.code);
  }
  return out;
}
```

## The Design That Tells the Object the Decision

In the second design, the rule lives inside the shipment object. Clients do not read fields;
they tell the object to depart and take back the result.

```js
// tell/record.mjs — shipments awaiting dispatch
export const RECORDS = [
  { code: "GN-1", state: "warehouse", paid: true, weight: 12 },
  { code: "GN-2", state: "warehouse", paid: true, weight: 45 },
  { code: "GN-3", state: "in_transit", paid: true, weight: 8 },
  { code: "GN-4", state: "warehouse", paid: false, weight: 5 },
  { code: "GN-5", state: "warehouse", paid: true, weight: 25 },
];
```

```js
// tell/shipment.mjs — the rule lives inside the object, in one place
const WEIGHT_LIMIT = 30;

export const shipment = (record) => ({
  code: record.code,
  depart() {
    if (record.state !== "warehouse") return { ok: false, reason: "not_in_warehouse" };
    if (record.paid === false) return { ok: false, reason: "unpaid" };
    if (record.weight > WEIGHT_LIMIT) return { ok: false, reason: "weight_limit" };
    return { ok: true, reason: "departed" };
  },
});
```

```js
// tell/web.mjs — tells the object the decision
export function dispatch(s) {
  return { httpStatus: s.depart().ok ? 200 : 409, code: s.code };
}
```

```js
// tell/batch.mjs — gives the same command
export const selected = (shipments) =>
  shipments.filter((s) => s.depart().ok).map((s) => s.code);
```

```js
// tell/courier.mjs — gives the same command; no chance to write the condition
export const list = (shipments) =>
  shipments.filter((s) => s.depart().ok).map((s) => s.code);
```

```js
// tell/scheduled.mjs — gives the same command
export function scan(shipments) {
  const out = [];
  for (const s of shipments) if (s.depart().ok) out.push(s.code);
  return out;
}
```

## Counting the Repetition

The first measure is how many files know the field names the rule depends on. A file is
counted as carrying a copy of the rule if it knows two or more of these fields; the data
file is excluded from this count.

```js
// rule-count.mjs — counts how many files know the fields the rule depends on
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";

const FIELDS = ["state", "paid", "weight"];
const root = process.argv[2];
let filesKnowing = 0;
let repeats = 0;

for (const d of readdirSync(root).filter((a) => a.endsWith(".mjs")).sort()) {
  const text = readFileSync(join(root, d), "utf8").replace(/^\/\/.*$/gm, "");
  const known = FIELDS.filter((a) => new RegExp(`\\b${a}\\b`).test(text));
  if (known.length > 0) filesKnowing += 1;
  if (known.length >= 2 && d !== "record.mjs") repeats += 1;
  console.log(`  ${d.padEnd(12)} known fields=${known.length}  ${known.join(" ")}`);
}
console.log(`${root.padEnd(6)} files knowing the fields=${filesKnowing}, files repeating the rule=${repeats}`);
```

```sh
node rule-count.mjs ask
node rule-count.mjs tell
```

```
  batch.mjs    known fields=3  state paid weight
  courier.mjs  known fields=2  state paid
  record.mjs   known fields=3  state paid weight
  scheduled.mjs known fields=3  state paid weight
  web.mjs      known fields=3  state paid weight
ask    files knowing the fields=5, files repeating the rule=4
  batch.mjs    known fields=0
  courier.mjs  known fields=0
  record.mjs   known fields=3  state paid weight
  scheduled.mjs known fields=0
  shipment.mjs known fields=3  state paid weight
  web.mjs      known fields=0
tell   files knowing the fields=2, files repeating the rule=1
```

Four files repeat the rule in the first design; one in the second. The scan also showed
where the rule had already cracked: the courier module knows only two of the three fields.

## The Divergent Result Set

The runtime counterpart of drift between copies is that clients working on the same data
produce different lists. The driver script runs all four clients on the same five shipments
and counts how many distinct results come out.

```js
// setup.mjs — four clients work with the same data, results are compared
const root = process.argv[2];
const { RECORDS } = await import(`./${root}/record.mjs`);
const { dispatch } = await import(`./${root}/web.mjs`);
const { selected } = await import(`./${root}/batch.mjs`);
const { list } = await import(`./${root}/courier.mjs`);
const { scan } = await import(`./${root}/scheduled.mjs`);

const input = root.startsWith("tell")
  ? RECORDS.map((await import(`./${root}/shipment.mjs`)).shipment)
  : RECORDS;

const results = {
  web: input.filter((r) => dispatch(r).httpStatus === 200).map((r) => r.code),
  batch: selected(input),
  courier: list(input),
  scheduled: scan(input),
};
for (const [name, arr] of Object.entries(results)) console.log(`${name.padEnd(9)} ${arr.join(" ")}`);
const distinct = new Set(Object.values(results).map((l) => l.join(" ")));
console.log(`${root}: distinct result set among clients = ${distinct.size}`);
```

```sh
node setup.mjs ask
node setup.mjs tell
```

```
web       GN-1 GN-5
batch     GN-1 GN-5
courier   GN-1 GN-2 GN-5
scheduled GN-1 GN-5
ask: distinct result set among clients = 2
web       GN-1 GN-5
batch     GN-1 GN-5
courier   GN-1 GN-5
scheduled GN-1 GN-5
tell: distinct result set among clients = 1
```

GN-2, at forty-five kilograms, shows up on the courier's list and nowhere else. In the
second design there is no room for this kind of drift, because there are no four separate
bodies of code to write the condition in.

## When the Rule Changes

The weight limit drops from 30 to 20. Both trees are copied and the change is applied to
each. The in-place edit is given a backup extension; GNU and BSD `sed` behave the same way
with this form.

```sh
cp -r ask ask-new
cp -r tell tell-new
sed -i.y 's/weight <= 30/weight <= 20/' ask-new/*.mjs
sed -i.y 's/WEIGHT_LIMIT = 30/WEIGHT_LIMIT = 20/' tell-new/shipment.mjs
rm -f ask-new/*.y tell-new/*.y
node setup.mjs ask-new
node setup.mjs tell-new
for k in ask tell; do echo "$k: edited files = $(diff -rq $k $k-new | grep -c '^Files')"; done
```

```
web       GN-1
batch     GN-1
courier   GN-1 GN-2 GN-5
scheduled GN-1
ask-new: distinct result set among clients = 2
web       GN-1
batch     GN-1
courier   GN-1
scheduled GN-1
tell-new: distinct result set among clients = 1
ask: edited files = 3
tell: edited files = 1
```

Three files against one. Notice also that the drift grew: the courier's list used to carry
only the extra GN-2, and once the limit dropped, GN-5 was added too. Once the rule's copies
diverge, the divergence is permanent; it grows with every change to the rule.

## The Limit of the Principle

Tell, don't ask does not forbid every field read. Handing a field out becomes a problem only
when a **decision** about that field is made outside the object. Reading the shipment code
to print it in a report is not a decision; looking at the shape of the code and deciding
whether the shipment is domestic is.

Overapplying the principle also produces a measurable cost. Adding a separate command method
to the object for every question grows the interface, and the bloat measured in the
Interface Segregation Principle lesson comes back. The criterion is not the number of
methods, but the number of files repeating the rule: if that number is greater than one, the
behavior sits in the wrong place.

## Summary

- The tell, don't ask principle asks that the work behind a decision be told to the object
  that owns the data, instead of taking its fields and deciding outside it.
- The number of files that know the rule's fields dropped from 5 to 2, and the number of
  files repeating the rule dropped from 4 to 1.
- Repetition produced drift: the four clients gave 2 distinct result sets on the same data,
  while the single-definition version produced 1.
- When the weight limit changed, 3 files were edited in the repeating design and 1 in the
  single-definition design; the drift grew along with the rule change.
- The principle limits deciding on a field from outside, not reading the field itself; the
  criterion is the number of files repeating the rule.

## Next Step

`depart` was named like a command, but it actually only returns a result: the shipment's
state does not change. A real dispatch operation should change the state too. What happens
when the same method both changes state and returns a value? Does calling the method twice
give the same result, does an extra call made just to write a log line change the system's
state? The next lesson demonstrates, with a runtime example, the surprise produced by an
operation that both changes state and returns a value, and establishes the measure for
separating the two responsibilities.
