Loading content...
Consider two developers implementing the same feature—applying a discount to an order. Watch how their approaches differ:
Developer A (The Interrogator):
const price = order.getPrice();
const discount = order.getDiscount();
const finalPrice = price - (price * discount);
order.setPrice(finalPrice);
Developer B (The Commander):
order.applyDiscount();
Both achieve the same result. But Developer A's code interrogates the order, extracts its internal state, performs calculations externally, and then shoves the result back in. Developer B simply tells the order to apply its discount.
This seemingly minor difference in style reflects a profound divergence in object-oriented thinking—and it determines whether your codebase will remain maintainable or devolve into procedural spaghetti wrapped in object syntax.
By the end of this page, you will understand the fundamental distinction between commands and queries, why 'telling' objects preserves encapsulation while 'asking' degrades it, and how to recognize when your code is interrogating objects rather than collaborating with them. You will develop the instinct to reach for commands first.
The Tell, Don't Ask (TDA) principle, articulated by Andy Hunt, Dave Thomas, and later championed by Martin Fowler, expresses a fundamental truth about object-oriented design:
Rather than asking an object for data and acting on that data, we should instead tell an object what to do.
This isn't merely a stylistic preference—it reflects the very purpose of objects in object-oriented programming. Objects exist to encapsulate data and the behavior that operates on that data. When you ask an object for its data and perform operations externally, you're treating the object as a passive data container. When you tell an object what to do, you're treating it as an active collaborator with responsibilities.
The principle follows directly from encapsulation: if an object's internal state is hidden (as it should be), the only way to interact with it is to tell it what to do. The moment you start asking for state, you're implicitly depending on the object's internal structure—and encapsulation begins to erode.
Objects should have behavior, not just data. When you find yourself extracting data from an object to make decisions externally, you're doing the object's job for it. The behavior should live where the data lives.
The Anthropomorphic Perspective:
Imagine objects as colleagues in a workplace. You wouldn't ask a colleague for their meeting notes, analyze them yourself, and then tell them what conclusions to draw. You'd say, 'Please summarize the key decisions from yesterday's meeting.' You tell them what outcome you need; they figure out how to produce it.
Similarly, you don't ask a bank account for its balance, check if it's sufficient, calculate the new balance, and then set it. You tell the account to withdraw an amount—it knows how to check balances, apply rules, and update its state.
This anthropomorphic thinking—treating objects as intelligent collaborators rather than dumb data buckets—is the essence of Tell, Don't Ask.
To understand Tell, Don't Ask deeply, we must first clarify the distinction between commands and queries. This distinction comes from Bertrand Meyer's Command-Query Separation (CQS) principle, which states:
The CQS principle says these two responsibilities should be separated: a method should either be a command or a query, but not both. TDA takes this further: prefer commands over queries in your design.
Let's examine each type in depth:
| Aspect | Commands (Tell) | Queries (Ask) |
|---|---|---|
| Purpose | Change object state | Retrieve object state |
| Return value | void (or success/failure indicator) | Data or computed value |
| Side effects | Intentional state change | None (ideally) |
| Encapsulation impact | Preserves—state changes internally | May expose—reveals internal structure |
| Coupling effect | Low—caller doesn't know internals | High—caller depends on returned data |
| Testability | Test by verifying outcomes | Test by verifying returned values |
| Example | account.withdraw(100) | account.getBalance() |
The Semantic Difference:
Beyond the technical distinction, commands and queries represent different intentions:
When you design with TDA in mind, you shift from 'What data do I need to accomplish X?' to 'What object should I tell to accomplish X?' This reframes the problem from data manipulation to responsibility assignment.
When you write code that queries an object and then does something with the result, stop and ask: 'Could the object do this itself?' If yes, you should probably tell the object to do it. Push the behavior to where the data lives.
Why does 'asking' cause problems? Let's examine the cascade of issues that emerge when code relies heavily on queries to extract state and perform external logic.
order.getCustomer().getAddress().getCity(). This creates fragile, deeply coupled code.A Concrete Example of Ask-Oriented Problems:
Consider this ask-oriented code for processing an order:
12345678910111213141516171819202122232425262728293031323334353637
// Ask-oriented: Problematicclass OrderProcessor { processOrder(order: Order): void { // Interrogating the order for its state const items = order.getItems(); const customer = order.getCustomer(); const paymentMethod = customer.getPaymentMethod(); const address = customer.getShippingAddress(); // Performing calculations externally let total = 0; for (const item of items) { const price = item.getPrice(); const quantity = item.getQuantity(); const discount = item.getDiscount(); total += price * quantity * (1 - discount); } // Checking customer status externally if (customer.getLoyaltyPoints() > 1000) { total *= 0.9; // 10% loyalty discount } // Setting state back after external computation order.setTotal(total); order.setStatus('processed'); // More external decision-making if (paymentMethod.getType() === 'credit') { this.chargeCredit(paymentMethod, total); } else if (paymentMethod.getType() === 'debit') { this.chargeDebit(paymentMethod, total); } this.shipTo(address); }}What's Wrong Here?
Order changes how it stores items, this code breaks.OrderProcessor is more interested in Order's data than its own responsibilities.Order, Customer, and PaymentMethod is scattered into OrderProcessor.This is procedural programming pretending to be OOP. The objects are just data carriers; the real work happens externally.
Now let's see the same functionality designed with Tell, Don't Ask. Instead of extracting data and computing externally, we tell each object to do its job:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
// Tell-oriented: Cleanclass OrderProcessor { processOrder(order: Order): void { // Tell the order to calculate and set its total order.calculateTotal(); // Tell the order to apply customer benefits order.applyCustomerBenefits(); // Tell the order to process payment order.processPayment(); // Tell the order to ship order.ship(); }} // The Order class encapsulates its own logicclass Order { private items: OrderItem[]; private customer: Customer; private total: number; private status: OrderStatus; calculateTotal(): void { // Order knows how to calculate its total this.total = this.items.reduce( (sum, item) => sum + item.calculateLineTotal(), 0 ); } applyCustomerBenefits(): void { // Tell customer to apply its loyalty discount this.total = this.customer.applyLoyaltyDiscount(this.total); } processPayment(): void { // Tell customer to pay (customer knows its payment method) this.customer.pay(this.total); } ship(): void { // Tell customer to receive shipment (customer knows address) this.customer.receiveShipment(this); this.status = 'shipped'; }} // Each item knows how to calculate its line totalclass OrderItem { private price: number; private quantity: number; private discount: number; calculateLineTotal(): number { return this.price * this.quantity * (1 - this.discount); }} // Customer knows its own loyalty logicclass Customer { private loyaltyPoints: number; private paymentMethod: PaymentMethod; private shippingAddress: Address; applyLoyaltyDiscount(amount: number): number { if (this.loyaltyPoints > 1000) { return amount * 0.9; } return amount; } pay(amount: number): void { // Tell payment method to charge this.paymentMethod.charge(amount); } receiveShipment(order: Order): void { // Shipping logic encapsulated this.shippingAddress.deliverTo(order); }}order.processPayment() expresses business intent; the call chain of getters and setters does not.applyLoyaltyDiscount().Notice how the tell-oriented design reads like a story of collaboration: 'Order, calculate your total. Now apply customer benefits. Now process payment. Now ship.' Each object plays its part. The procedural version reads like interrogation: 'Order, what are your items? Customer, what's your loyalty status? Payment method, what's your type?' Then we do all the work ourselves.
One of the most reliable indicators that you're violating Tell, Don't Ask is the code smell known as Feature Envy. A method exhibits feature envy when it is more interested in the data of another class than its own.
Martin Fowler describes it as: "A method that seems more interested in a class other than the one it is in."
When a method makes extensive use of getters from another object to perform calculations, that method envies the features of the other object. The solution is almost always to move the method (or parts of it) to the class whose data it's using.
Detecting Feature Envy in Practice:
Here's a quick heuristic: if a method uses more data from another class than from its own, it probably belongs in that other class. The refactoring is usually to:
This is the essence of Tell, Don't Ask: move behavior to where the data lives.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// Feature Envy: This method envies Customer's dataclass PricingService { calculateShippingCost(customer: Customer): number { // Excessive querying of Customer const address = customer.getAddress(); const country = address.getCountry(); const postalCode = address.getPostalCode(); const isPremium = customer.isPremiumMember(); const orderHistory = customer.getOrderHistory(); const totalSpent = orderHistory.getTotalSpent(); // Complex logic using Customer's data if (country === 'USA') { if (postalCode.startsWith('9')) { return isPremium ? 5 : 10; // West coast } return isPremium ? 7 : 12; } // ... more logic envying Customer's data return 15; }} // Tell, Don't Ask: Move the logic to Customerclass Customer { private address: Address; private premiumMember: boolean; private orderHistory: OrderHistory; calculateShippingCost(): number { // Customer knows its own shipping cost rules const baseCost = this.address.getBaseShippingCost(); return this.premiumMember ? baseCost * 0.5 : baseCost; }} class Address { private country: string; private postalCode: string; getBaseShippingCost(): number { // Address knows regional shipping costs if (this.country === 'USA') { return this.postalCode.startsWith('9') ? 10 : 12; } return 15; }} // Clean caller codeclass PricingService { calculateShippingCost(customer: Customer): number { return customer.calculateShippingCost(); // Tell, don't ask! }}Tell, Don't Ask doesn't exist in isolation—it's deeply connected to other object-oriented design principles. Understanding these relationships helps you apply TDA more effectively and see how good practices reinforce each other.
| Principle | Relationship with TDA | How They Reinforce Each Other |
|---|---|---|
| Encapsulation | TDA is encapsulation in action | TDA ensures that data and behavior are bundled together, which is the definition of encapsulation |
| Law of Demeter | TDA prevents Demeter violations | When you tell objects what to do, you don't need to reach through object chains (a.getB().getC()) |
| Single Responsibility (SRP) | TDA helps identify responsibilities | When you see feature envy, the behavior belongs elsewhere—SRP helps you figure out where |
| Open/Closed (OCP) | TDA enables extension | Tell-based code can be extended through polymorphism; ask-based code often requires modification |
| Liskov Substitution (LSP) | TDA leverages polymorphism | When you tell an object to do something, subtypes can do it their own way without breaking callers |
| Dependency Inversion (DIP) | TDA works through abstractions | Telling objects to act works best when you depend on interfaces, not concrete types |
| DRY | TDA centralizes logic | When behavior lives in one place (the object), it's not duplicated across callers |
Notice how these principles form a reinforcing web. Violating TDA often leads to violating Law of Demeter, breaking encapsulation, and duplicating logic (violating DRY). Conversely, following TDA naturally promotes many other good practices. Principles aren't isolated rules—they're facets of the same underlying philosophy of good design.
TDA and Information Expert (GRASP):
The GRASP pattern Information Expert states: assign a responsibility to the class that has the information necessary to fulfill it.
TDA is essentially Information Expert in action. When you ask 'Who should do X?', Information Expert says 'The class that has the data to do X.' TDA says 'Don't extract that data; tell that class to do X.'
They're two sides of the same coin—one focused on responsibility assignment, the other on interaction style. Together, they guide you to designs where behavior naturally gravitates to where the data lives.
Developing the instinct to spot TDA violations requires practice. Here are patterns and code smells that indicate ask-heavy, tell-deficient code:
obj.getA().getB().getC().getValue() — You're navigating through objects to extract data.x = obj.getX(); x += 1; obj.setX(x); — You're doing the object's job externally.if (obj.getStatus() === 'active') { ... } — You're making decisions based on extracted state.CustomerData or OrderInfo that are just data holders.if (obj.getType() === ...) appears in multiple places.process(order.getItems(), order.getCustomer(), order.getDiscount()) — Why not just pass order and tell it what to do?The Getter Explosion Test:
Count the getters in a class. If a class has more getters than behavior methods, it's likely a data holder rather than an object with responsibilities. True objects have methods that do things, not just methods that expose things.
As a rule of thumb:
The goal isn't zero getters (some are inevitable), but a healthy ratio where behavior dominates over data access.
The ultimate expression of ask-heavy code is the Anemic Domain Model, where domain objects are just data structures with getters and setters, and all business logic lives in 'service' or 'manager' classes. This is procedural programming dressed in object syntax. TDA is the antidote—it pushes behavior back into domain objects where it belongs.
We've explored the foundational concepts of Tell, Don't Ask. Let's consolidate the key insights:
What's Next:
Now that we understand the conceptual foundation—commands over queries—we'll explore the complementary idea of pushing behavior to data. While Tell, Don't Ask focuses on the interaction style (tell vs ask), pushing behavior to data focuses on where behavior should live. Together, these concepts form a complete picture of well-designed object-oriented systems.
You now understand the fundamental distinction between commands and queries, why asking objects for state leads to design degradation, and how Tell, Don't Ask preserves encapsulation and promotes cohesive, maintainable code. Next, we'll explore how to push behavior to where data lives.