The idea
What it is
Tell, Don't Ask is a simple rule about where a decision lives. Don't reach into an object, pull out its data, make a decision, and then push the result back. Instead, tell the object what you want done and let it use its own data to decide. The behaviour should sit next to the data it needs — inside the same object.
Think about a good manager. A good manager doesn't grab a worker's tools and do the task themselves. They tell the worker the goal — "ship this order today" — and let the worker, who knows their own tools and situation, handle the details. A bad manager micromanages: they ask for every detail, decide everything, and hand back instructions. Code that writes if (account.getBalance() >= amount) account.setBalance(account.getBalance() - amount) is that bad manager — it pulls the balance out of the account, decides, and pushes a new balance back, doing the account's own job for it.
The one sentence to remember
Don't ask an object for its data and then make a decision about it. Tell the object what you want, and let it decide using the data it already owns. Behaviour belongs next to the data.
It's a heuristic, not a law
Tell, Don't Ask is a guideline, not an absolute rule. Objects still legitimately have getters: code that genuinely just reports state — showing a balance on a screen, formatting a report, logging a value — is fine. The smell is asking for data to make a decision the object could have made itself. Don't contort code to avoid every getter; apply the rule where a decision should have lived inside the object.
Mechanics
How it works
The smell: asking for data, then deciding for the object
Here is the trap. A caller wants to withdraw money. It asks the account for its balance, checks the rule itself, then sets a new balance:
// ⚠️ ASK — the caller pulls the balance out, decides, then pushes a result back.
if (account.getBalance() >= amount) {
account.setBalance(account.getBalance() - amount);
}Notice who is doing the thinking. The caller knows the overdraft rule (balance >= amount). The account is just a dumb data bag with a getter and a setter — it holds a number but enforces nothing. The real rule about what a valid balance is now lives outside the thing it's about.
Why that hurts: the rule gets copied, then drifts
The moment a second caller also needs to withdraw — an ATM and an online checkout, say — that same if check gets copied into both. Now the overdraft rule lives in two places, and nothing keeps them in sync:
- Duplication — the same
balance >= amountrule is written in every caller that withdraws. Add a third caller and it's written a third time. - Drift — when the rule changes (say, allow a $100 overdraft buffer), you must find and edit every copy. Miss one and the callers now disagree about what's legal.
- Broken encapsulation — the account exposes
getBalanceandsetBalance, so any caller can set the balance to anything, bypassing the rule entirely. The object can't protect its own invariant. - Inconsistency — if one caller simply forgets the check, it can drive the balance negative while another caller refuses. The same account behaves differently depending on who's asking.
A getter + a setter = a rule with no home
When an object exposes both getX() and setX() with no behaviour between them, every caller has to remember the rule that connects them. The object becomes a passive data holder, and the real logic is smeared across the whole codebase — impossible to enforce, easy to break.
The fix: tell the object, and put the rule inside it
Don't ask for the balance — tell the account to withdraw. Move the decision into the account, where the data already lives:
// ✓ TELL — one method on the account; the rule lives WITH the data.
account.withdraw(amount);
class BankAccount {
private balance: number;
withdraw(amount: number) {
if (amount > this.balance) throw new Error("Insufficient funds");
this.balance -= amount; // the rule and the data are in one place
}
}Now there is exactly one copy of the overdraft rule, and it lives inside the account — right next to the balance it protects. Every caller, ATM or checkout, calls the same account.withdraw(amount). The rule can't drift, because there's only one of it. And the account no longer needs a public setBalance, so no caller can bypass the check. The object guards its own invariant.
Where the decision lives — the whole idea in one picture
The difference is purely about location. In Ask, the decision (the if) sits outside the object, in the caller. In Tell, the very same decision sits inside the object, as a method. Same check, same data — just moved to where it belongs.
balance >= amount?) sits inside the caller, which reaches into BankAccount through getBalance() and setBalance(). The account is a dumb data bag, and the rule has to be copied into every caller — so it can drift. Right (Tell): the caller shrinks to account.withdraw(x), and the same decision diamond now lives inside BankAccount as withdraw(amount). One copy of the rule, sitting right next to the balance it guards.How this connects to encapsulation and Law of Demeter
- Encapsulation — Tell, Don't Ask is encapsulation taken seriously. An object should hide its data and the rules about that data. Exposing
getBalance/setBalanceleaks both; exposingwithdrawkeeps them inside. - Law of Demeter — its close cousin. Demeter says don't reach through an object to talk to strangers (
order.getCustomer().getWallet()...); Tell, Don't Ask says don't reach into an object to grab its data and decide for it. Both push behaviour next to the data it needs, so you tell one friend (order.chargeCard(amount)) instead of digging. - Rich objects, not data bags — following the rule turns anaemic objects (all getters/setters, no behaviour) into ones that actually do things and protect their own state.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
A BankAccount box holding a balance, with two callers — an ATM and an OnlineCheckout — that both need to withdraw money. In Ask mode the decision diamond (balance >= amount?) sits outside the account, copied into both caller boxes: each one reads getBalance(), decides for itself, then calls setBalance(). An indicator flags rule lives in 2 places · can drift. Click Move the rule inside and the diamond slides into the BankAccount, becoming withdraw(amount); the callers shrink to a single account.withdraw(x) line and the indicator drops to rule lives in 1 place. Then trigger an overdraft attempt: in Ask mode one caller forgot the check and lets the balance go negative (inconsistent); in Tell mode the account rejects it uniformly. One fixed panel, replaced each interaction; nothing scrolls away.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
See the rule copied into two callers
Open the prototype in Ask mode. The balance >= amount? decision diamond sits outside the BankAccount, copied into both caller boxes — the ATM and the OnlineCheckout. Each one reads getBalance(), checks the rule itself, then calls setBalance(). Read the indicator: rule lives in 2 places · can drift. The account is just a data bag holding a number; it enforces nothing.
Trigger an overdraft and watch it drift
In Ask mode, hit Overdraft attempt. One caller still checks the rule and refuses — but the other forgot the if, so it calls setBalance() straight through and drives the balance negative. Read the panel: the same account now behaves inconsistently depending on who asked, because the rule lived outside it and one copy was wrong.
Move the rule inside
Click Move the rule inside. Watch the decision diamond slide into the BankAccount, becoming withdraw(amount), while both callers shrink to a single account.withdraw(x) line. The indicator drops to rule lives in 1 place. Now trigger Overdraft attempt again: the account rejects it uniformly, no matter which caller asked, because there is exactly one copy of the rule and it lives next to the data. That's Tell, Don't Ask.
In practice
When to use it — and what trips people up
Reach for it when a caller decides something the object should own
Tell, Don't Ask is most useful when you catch a caller pulling data out of an object, applying a rule, and pushing a result back — especially if that rule appears in more than one caller. Move the rule, and the data it protects, into the object.
- A get/check/set triple —
if (x.getY() ...) x.setY(...). The rule connecting the getter and setter belongs inside the object as a single method. - A rule copied across callers — the same validation or calculation written in several places. One method on the object means one copy that can't drift.
- Invariants that must hold — a balance that can't go negative, a status that must follow a sequence. Only the object can guarantee its own invariant, so the decision must live inside it.
- Anaemic domain objects — classes that are all getters and setters with no behaviour. Pushing decisions into them turns data bags into real objects.
Don't turn it into 'never write a getter'
Queries that genuinely just report state are legitimate and necessary. A UI must read a balance to display it; a report formats values; a log records them. Those are reads, not decisions the object should own — Tell, Don't Ask isn't violated by them. The smell is asking for data to make a decision the object itself could make. Apply the rule there; don't contort code to eliminate every getter, or you'll just add awkward methods nobody needs.
What it gives you
- One copy of each rule — the logic lives with the data, so it can't be duplicated across callers or drift out of sync.
- Real encapsulation — the object hides its data and the rules about it; without a public setter, no caller can put it into an invalid state.
- Consistency — every caller goes through the same method, so the same account behaves identically no matter who asks.
- Richer objects — behaviour lands next to data, turning anaemic data bags into objects that actually do their own work.
Common mistakes
- Can be over-applied — treated as 'never expose a getter', it breeds awkward methods and hides state that callers legitimately need to read.
- Not a fit for pure data — DTOs, config trees, and value objects are meant to be read; forcing 'tell' methods onto them adds noise.
- Pushing every decision in can bloat an object — sometimes a rule truly belongs to a coordinator/service, not the entity, and over-telling overloads the object.
- Reporting and display still need queries — you can't avoid reads entirely, so the rule is a judgement call, not a mechanical transform.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
// BEFORE — ASK: every caller pulls the balance out and decides for the account.
class BankAccountV0 {
constructor(private balance: number) {}
getBalance() { return this.balance; }
setBalance(b: number) { this.balance = b; } // ⚠️ anyone can set anything
}
function atmWithdraw(account: BankAccountV0, amount: number) {
// the overdraft rule lives HERE, in the caller...
if (account.getBalance() >= amount) {
account.setBalance(account.getBalance() - amount);
}
}
function checkoutWithdraw(account: BankAccountV0, amount: number) {
// ...and is copied AGAIN here. Forget the check and the balance goes negative.
if (account.getBalance() >= amount) {
account.setBalance(account.getBalance() - amount);
}
}
// AFTER — TELL: one method on the account; the rule lives WITH the data.
class BankAccount {
constructor(private balance: number) {}
withdraw(amount: number) {
if (amount > this.balance) throw new Error("Insufficient funds");
this.balance -= amount; // rule + data in one place, can't drift
}
getBalance() { return this.balance; } // a query is fine — it just reports state
}
const account = new BankAccount(100);
account.withdraw(30); // ATM and checkout both just TELL the account.
account.withdraw(30); // No setBalance to bypass; the account guards itself.References & further reading
6 sources- Articlemartinfowler.com
TellDontAsk — Martin Fowler
Fowler's short, definitive bliki entry: what the principle means, how it relates to object-oriented design and the Law of Demeter, and a candid note that it's a guideline he doesn't follow blindly.
- Papermedia.pragprog.com
The Pragmatic Programmer — 'The Art of Enbugging' (Hunt & Thomas)
The IEEE Software column where Hunt and Thomas coined 'Tell, Don't Ask', tying it to decoupling and the Law of Demeter with a worked example of keeping decisions next to the data.
- Articlewiki.c2.com
TellDontAsk — Portland Pattern Repository (c2 wiki)
A long, opinionated community thread that argues both sides — when telling genuinely improves a design, and when chasing 'no getters' becomes dogma.
- Articleblog.ploeh.dk
Tell Don't Ask — Mark Seemann (ploeh blog)
Seemann's writing connects Tell, Don't Ask to encapsulation and invariants — why an object that owns its rules can guarantee it's always in a valid state.
- Articlebaeldung.com
Avoid Getters and Setters Whenever Possible — Baeldung
A code-first Java walkthrough of why exposing getters/setters leaks behaviour, and how moving rules into the object (the 'tell' style) protects its invariants.
- Talkyoutube.com
Tell, Don't Ask — object design explained (video)
A short, visual walkthrough that refactors a get/check/set caller into a single 'tell' method and shows the rule collapsing into one place.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Sign in to save your best score.
question 01 / 05
What does 'Tell, Don't Ask' tell you to do?
question 02 / 05
Why is if (account.getBalance() >= amount) account.setBalance(account.getBalance() - amount) a smell?
question 03 / 05
What's the standard fix for that get/check/set pattern?
question 04 / 05
Does 'Tell, Don't Ask' mean an object should never have a getter?
question 05 / 05
Which principle is the closest cousin of Tell, Don't Ask?
0/5 answered