Tell, Don't Ask Principle Explained with Java Examples

Introduction

This tutorial explains the Tell, Don't Ask principle with Java examples. We will start with what the principle says and where it comes from. We will then build the same leave-booking feature twice - once by asking an object for its data and deciding outside it, and once by telling the object what to do - and see the "ask" version end up with a negative leave balance. Lastly, we will look at when asking is perfectly fine, and how the principle relates to the Law of Demeter and Command-Query Separation.

What is Tell, Don't Ask

Tell, Don't Ask says: rather than asking an object for its data and then making decisions about that data, tell the object what you want done, and let it decide how. The idea is usually traced to Alec Sharp's book Smalltalk by Example (1997): "Procedural code gets information then makes decisions. Object-oriented code tells objects to do things." Andy Hunt and Dave Thomas later made the name popular.

The principle is really about where a rule lives. When callers read an object's state, decide, and write the result back, the rule lives in the callers - in as many copies as there are callers. When the object makes the decision itself, the rule lives in one place, next to the data it protects. That is encapsulationClick to read: encapsulation in its full sense: hiding not only the fields, but also the decisions about them.
Tell Dont Ask principle - asking an employee for its leave balance and deciding in the caller versus telling the employee to take leave

Example - asking versus telling

An employee has a leave balance, and leave can only be booked if enough days are left. In the "ask" version, EmployeeData has a getter and a setter, and two callers - a leave screen and a bulk import job - each implement the booking. In the "tell" version, Employee has a takeLeave() method and nothing else can change the balance.
Java code comparing the ask style and the tell style for booking leave
//TellDontAskExample.java
public class TellDontAskExample {

  // ---------- Ask style: data with a getter and a setter ----------
  static class EmployeeData {
    private final String name;
    private int leaveBalance;

    EmployeeData(String name, int leaveBalance) {
      this.name = name;
      this.leaveBalance = leaveBalance;
    }

    String getName() { return name; }
    int getLeaveBalance() { return leaveBalance; }
    void setLeaveBalance(int leaveBalance) { this.leaveBalance = leaveBalance; }
  }

  // Caller 1 remembers the rule
  static class LeaveScreen {
    void book(EmployeeData e, int days) {
      if (e.getLeaveBalance() >= days) {
        e.setLeaveBalance(e.getLeaveBalance() - days);
        System.out.println("LeaveScreen booked " + days + " days for " + e.getName());
      } else {
        System.out.println("LeaveScreen refused: not enough leave");
      }
    }
  }

  // Caller 2, written later by someone else, forgets it
  static class BulkImport {
    void book(EmployeeData e, int days) {
      e.setLeaveBalance(e.getLeaveBalance() - days);
      System.out.println("BulkImport booked " + days + " days for " + e.getName());
    }
  }

  // ---------- Tell style: the object enforces its own rule ----------
  static class Employee {
    private final String name;
    private int leaveBalance;

    Employee(String name, int leaveBalance) {
      this.name = name;
      this.leaveBalance = leaveBalance;
    }

    void takeLeave(int days) {
      if (days <= 0) {
        throw new IllegalArgumentException("Days must be positive: " + days);
      }
      if (days > leaveBalance) {
        throw new IllegalStateException(name + " has " + leaveBalance
            + " days left, cannot take " + days);
      }
      leaveBalance -= days;
    }

    int leaveBalance() { return leaveBalance; }  // a query for display is fine
  }

  public static void main(String[] args) {
    EmployeeData harry = new EmployeeData("Harry Major", 10);
    new LeaveScreen().book(harry, 7);
    new BulkImport().book(harry, 7);
    System.out.println("Harry's balance, ask style : " + harry.getLeaveBalance());

    Employee nancy = new Employee("Nancy Smith", 10);
    nancy.takeLeave(7);
    try {
      nancy.takeLeave(7);
    } catch (IllegalStateException e) {
      System.out.println("Refused: " + e.getMessage());
    }
    System.out.println("Nancy's balance, tell style: " + nancy.leaveBalance());
  }
}
 OUTPUT of the above code
LeaveScreen booked 7 days for Harry Major
BulkImport booked 7 days for Harry Major
Harry's balance, ask style : -4
Refused: Nancy Smith has 3 days left, cannot take 7
Nancy's balance, tell style: 3

Explanation of the code

  • EmployeeData exposes its balance for reading and writing, so the rule "you cannot book more leave than you have" has to be written by each caller. LeaveScreen wrote it; BulkImport did not, and Harry ended up with a negative balance.
  • Employee has no setter. The only way to change the balance is takeLeave(), which checks the rule every time - no caller can forget it, and no caller can get it slightly wrong.
  • When the request is refused, the exception message says why. The decision and its explanation both come from the object that has the information.
  • leaveBalance() is still available, because the screen needs to show the balance. Tell, Don't Ask does not forbid reading data; it forbids reading data in order to make a decision that the object should make itself.
  • If the rule changes - for example, allowing up to 2 days of negative balance for senior staff - only takeLeave() changes.

Signs that you are asking instead of telling

  • Get, decide, set: code that calls a getter, tests the value, and calls a setter on the same object.
  • The same if in several places: two classes check getStatus() == ACTIVE before doing something to an object.
  • Feature envy: a method that uses another object's data more than its own. It usually wants to move into that object.
  • Anemic domain model: domain classes that are only fields, getters and setters, with all the logic in "service" classes. Martin Fowler named this anti-pattern in 2003.

When asking is the right choice

Tell, Don't Ask is a guide, not a ban on getters -
  • Displaying and reporting: a screen, report or API response needs the data itself. Asking for it to show it is fine.
  • Decisions that belong to the caller: choosing a colour for an overdue invoice is a presentation decision. The invoice should report that it is overdue; it should not know about colours.
  • Data carriers: records and DTOs exist to move data between layers. They have no rules of their own to protect.
  • Combining several objects: when a decision needs data from many objects, no single object can make it. A coordinating method asks each for what it needs, and tells each what to do.

Related principles

Summary

In this tutorial we first saw that Tell, Don't Ask means telling an object what to do rather than reading its data and deciding outside it, so that each rule lives in one place next to its data. Next we booked leave in two styles: with a getter and setter, a second caller forgot the rule and produced a negative balance; with a takeLeave() method, the rule could not be bypassed. We then listed the signs of asking instead of telling. We then saw when asking is the right choice - display, caller-owned decisions, data carriers and coordination. Lastly, we related the principle to the Law of Demeter, Command-Query Separation and Design by Contract.