Loading learning content...
Throughout this module, we've explored when to use getters, when to avoid setters, and how accessors relate to encapsulation. Now we'll crystallize these insights into a powerful design principle that provides a memorable decision-making tool: Tell, Don't Ask.
This principle, first articulated by Andy Hunt and Dave Thomas in their "Pragmatic Programmer" work and refined in the object-oriented design community, captures the essence of good encapsulation in four simple words. Once you internalize it, you'll find yourself naturally designing more robust, maintainable, and truly object-oriented systems.
The principle states: Instead of asking an object for data and then acting on that data, tell the object what to do.
This simple reframing transforms how you think about object responsibilities and leads naturally to designs where behavior lives with the data it operates on.
By the end of this page, you will understand the Tell, Don't Ask principle, recognize patterns that violate it, learn refactoring strategies that honor it, and understand where the principle has limits. You'll have a practical tool for making design decisions about object responsibilities.
The Ask Pattern (Anti-pattern):
In the "Ask" pattern, code retrieves information from an object and then makes decisions based on that information:
1. Get data from object
2. Examine the data
3. Make a decision
4. Possibly modify the object or take other action
This pattern seems natural—after all, we need information to act. But it has a critical flaw: it puts the decision-making logic in the wrong place.
The Tell Pattern (Preferred):
In the "Tell" pattern, code tells the object what to do and trusts it to handle the details:
1. Tell object what outcome you want
2. Object makes decisions internally
3. Object acts or responds appropriately
This pattern keeps the logic close to the data it operates on.
1234567891011121314
// ❌ ASK Pattern// "Let me check your state and decide" if (account.getBalance() < amount) { throw new InsufficientFundsException();}account.setBalance( account.getBalance() - amount);transactionLog.record( account.getId(), "withdrawal", amount);12345678910
// ✅ TELL Pattern// "Please handle this withdrawal" account.withdraw(amount); // The object handles:// - Balance check// - Balance update// - Transaction logging// - Any other invariantsWhy Tell is Better:
Single Responsibility: The withdrawal logic lives in one place—the BankAccount class—not scattered across callers.
Encapsulation Preserved: Callers don't need to know about balance, transaction logs, or validation rules. The internal implementation is hidden.
Consistency Guaranteed: Every withdrawal goes through the same logic. No caller can forget the transaction log or skip validation.
Easier Evolution: If withdrawal rules change (overdraft protection, daily limits), you modify one place.
Command Intent: withdraw(amount) expresses business intent. setBalance(getBalance() - amount) expresses mechanism.
Think of objects as intelligent agents that know how to do their job, not as data bags that you manipulate from outside. You're delegating work to a capable entity, not micromanaging a passive container.
Ask patterns often hide in plain sight. Here are common signatures to watch for:
if (obj.getX() == condition) { doSomething(obj); }obj, not in the callerobj make this decision? Can obj perform the action?1234567891011
// ❌ ASK: Getter + conditionalif (order.getStatus() == OrderStatus.PENDING) { order.setStatus(OrderStatus.CONFIRMED); emailService.sendConfirmation(order.getCustomerEmail()); inventoryService.reserve(order.getItems());} // ✅ TELL: Object handles its own state transitionorder.confirm(emailService, inventoryService);// Order internally checks preconditions, updates state,// and coordinates side effectsresult = calculate(obj.getA(), obj.getB(), obj.getC())12345678910111213
// ❌ ASK: Extracting data to calculate externallydouble tax = calculateTax( order.getSubtotal(), order.getShippingAddress().getState(), order.getItems().stream() .map(Item::getTaxCategory) .collect(toList()));order.setTax(tax); // ✅ TELL: Object knows how to calculate its own taxorder.calculateTax(taxService);// Order provides its data to taxService internallyobj.getA().getB().getC()12345678910111213
// ❌ ASK: Chain reveals deep structureif (employee.getDepartment().getManager().getApprovalLimit() >= request.getAmount()) { request.approve();} // ✅ TELL: Ask the right objectif (employee.canApprove(request)) { request.approve();} // Or even better, tell the request to seek approvalrequest.requestApprovalFrom(employee);if (obj.getX() != null && obj.getX().getY() != null)12345678910111213
// ❌ ASK: Null checks cascadeString city = "Unknown";if (user.getAddress() != null) { if (user.getAddress().getCity() != null) { city = user.getAddress().getCity(); }} // ✅ TELL: Object handles the complexityString city = user.getCityOrDefault("Unknown"); // Or with OptionalString city = user.getCity().orElse("Unknown");123456789101112131415161718192021222324252627282930
// ❌ ASK: This method is envious of Orderclass ReportGenerator { public String generateOrderSummary(Order order) { // Uses only Order's data return String.format( "Order %s: %d items, total $%.2f, status: %s", order.getId(), order.getItems().size(), order.getTotal().doubleValue(), order.getStatus() ); }} // ✅ TELL: Move the method to where the data livesclass Order { public String getSummary() { return String.format( "Order %s: %d items, total $%.2f, status: %s", id, items.size(), total.doubleValue(), status ); }} // Or use a formatting service that Order callsclass Order { public String format(OrderFormatter formatter) { return formatter.format(this); // Double dispatch }}When you spot an Ask pattern, here are techniques to transform it into a Tell pattern:
1234567891011121314151617181920212223242526272829303132
// BEFORE: Logic in callerpublic void applyDiscount(Order order, DiscountCode code) { if (order.getTotal() >= code.getMinimum() && order.getCustomer().isMember() && code.isValid()) { double discount = order.getTotal() * code.getPercentage(); order.setDiscount(discount); }} // AFTER: Logic moved to Orderpublic class Order { public void applyDiscount(DiscountCode code) { if (!canApplyDiscount(code)) { return; // Or throw } this.discount = calculateDiscount(code); } private boolean canApplyDiscount(DiscountCode code) { return total.compareTo(code.getMinimum()) >= 0 && customer.isMember() && code.isValid(); } private Money calculateDiscount(DiscountCode code) { return total.multiply(code.getPercentage()); }} // Caller just tellsorder.applyDiscount(code);12345678910111213141516171819202122232425262728293031323334353637383940414243
// BEFORE: Type check + switchvoid processPayment(Payment payment) { switch (payment.getType()) { case CREDIT_CARD: processCreditCard( payment.getCardNumber(), payment.getAmount() ); break; case PAYPAL: processPayPal( payment.getEmail(), payment.getAmount() ); break; case BANK_TRANSFER: processBankTransfer( payment.getAccountNumber(), payment.getAmount() ); break; }} // AFTER: Polymorphic behaviorinterface Payment { void process(PaymentProcessor processor);} class CreditCardPayment implements Payment { public void process(PaymentProcessor processor) { processor.chargeCreditCard(cardNumber, amount); }} class PayPalPayment implements Payment { public void process(PaymentProcessor processor) { processor.chargePayPal(email, amount); }} // Caller just tellspayment.process(processor);1234567891011121314151617181920212223242526272829303132333435363738394041
// BEFORE: Expose data for external processingpublic class Person { private String firstName; private String lastName; private LocalDate birthDate; private Address address; // Getters for everything public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public LocalDate getBirthDate() { return birthDate; } public Address getAddress() { return address; }} // External code assembles the dataString report = String.format("%s %s, born %s, lives at %s", person.getFirstName(), person.getLastName(), person.getBirthDate(), person.getAddress()); // AFTER: Accept a visitor/formatterpublic class Person { private String firstName; private String lastName; private LocalDate birthDate; private Address address; public void describeTo(PersonFormatter formatter) { formatter.addName(firstName, lastName); formatter.addBirthDate(birthDate); address.describeTo(formatter); // Tell pattern continues }} // UsagePersonFormatter formatter = new ReportFormatter();person.describeTo(formatter);String report = formatter.getResult();getStatus(), provide isActive(), isPending(), canBeEdited()12345678910111213141516171819202122232425
// BEFORE: Expose status, caller interpretsif (order.getStatus() == OrderStatus.PENDING || order.getStatus() == OrderStatus.ON_HOLD) { showEditButton();} // AFTER: Object knows what status meansif (order.canBeEdited()) { showEditButton();} // In Order classpublic boolean canBeEdited() { return status == OrderStatus.PENDING || status == OrderStatus.ON_HOLD;} public boolean isAwaitingPayment() { return status == OrderStatus.PENDING && paymentStatus == PaymentStatus.UNPAID;} public boolean canBeCancelled() { return !isShipped() && !isDelivered();}Like all principles, Tell, Don't Ask has limits. Understanding these nuances helps you apply it wisely rather than dogmatically.
1234567891011121314151617181920212223
// Acceptable: UI needs to display datapublic class OrderView { public void render(Order order) { // Presentation needs data to display display("Order #" + order.getId()); display("Status: " + order.getStatusLabel()); display("Total: " + order.getFormattedTotal()); // Not making business decisions with this data }} // Even better: Order provides a view modelpublic class Order { public OrderViewDTO toViewDTO() { return new OrderViewDTO( id, status.getLabel(), total.format(), getItemDescriptions() ); }}123456789101112131415161718192021
// When decision spans objects, use a domain servicepublic class ShippingCalculator { public Money calculateShipping( Order order, Carrier carrier, Warehouse warehouse) { // This decision needs data from multiple objects // It's okay to ask each object for relevant info Distance distance = warehouse.distanceTo( order.getShippingAddress() ); Weight weight = order.getTotalWeight(); return carrier.calculateRate(distance, weight); }} // The key: this logic is in ONE place (the service)// Not scattered across multiple callersTell, Don't Ask is a guideline, not a law. Apply it to drive business logic into objects where it belongs. Don't apply it so rigidly that you create awkward APIs or violate other good design principles. The goal is maintainable, understandable code—not religious adherence to any single principle.
Let's see a complete example of refactoring code from Ask-heavy to Tell-based design:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
// Ask-heavy code: logic scattered in callerspublic class OrderService { public void submitOrder(Order order, PaymentInfo payment) { // ASK: Check if order has items if (order.getItems().isEmpty()) { throw new EmptyOrderException(); } // ASK: Calculate total (reaching into order internals) double total = 0; for (OrderItem item : order.getItems()) { total += item.getPrice() * item.getQuantity(); } // ASK: Apply discounts if customer is premium if (order.getCustomer().getMembershipLevel() == MembershipLevel.PREMIUM) { total = total * 0.9; // 10% off } // ASK: Check stock for (OrderItem item : order.getItems()) { if (inventoryService.getStock(item.getProductId()) < item.getQuantity()) { throw new InsufficientStockException(); } } // ASK: Validate payment if (payment.getCardNumber() == null || payment.getCardNumber().length() != 16) { throw new InvalidPaymentException(); } // Process payment paymentService.charge(payment.getCardNumber(), total); // Update order status order.setStatus(OrderStatus.SUBMITTED); order.setSubmittedAt(LocalDateTime.now()); order.setTotal(total); // Reserve inventory for (OrderItem item : order.getItems()) { inventoryService.reserve( item.getProductId(), item.getQuantity() ); } // Send confirmation emailService.send( order.getCustomer().getEmail(), "Order Confirmation", "Your order #" + order.getId() + " has been submitted." ); }}Use this guide to quickly identify and refactor Ask patterns:
| You See... | Ask For... | Tell Instead... |
|---|---|---|
if (obj.getX() == Y) | State to check | if (obj.isY()) or obj.doIfY() |
obj.setX(obj.getX() + 1) | Value to modify | obj.incrementX() or obj.addToX(1) |
calculate(obj.getA(), obj.getB()) | Data for calculation | obj.calculate() or obj.getCalculated() |
obj.getA().getB().getC() | Deeply nested data | obj.getC() (add delegation method) |
for (x : obj.getList()) | Collection to iterate | obj.forEach(action) or obj.doForEach() |
obj.setStatus(X); obj.setTime(Y) | Multiple fields to set | obj.transitionTo(X) (handles both) |
When you're about to write obj.getX(), pause and ask: "What am I going to DO with X?" If the answer involves a decision or action, that logic probably belongs in obj.
The Tell, Don't Ask principle captures the essence of good object-oriented encapsulation. Here are the key takeaways:
canEdit() instead of exposing status for callers to interpret.Module Complete:
You've now completed the module on Getters and Setters. You understand why accessors exist, when getters are appropriate, when to avoid setters, and how the Tell, Don't Ask principle ties these concepts together into actionable design guidance.
The key insight from this entire module: Objects should be active participants with behavior, not passive data bags that external code manipulates. When you design with this mindset, encapsulation becomes natural, code becomes more maintainable, and objects become more powerful.
Congratulations! You've mastered the principles behind getters and setters. You now have a framework for deciding when to expose data, when to keep it private, and how to design objects that own their behavior. The Tell, Don't Ask principle will serve as a guiding light in your object-oriented design journey.