Table of contents
- TDA — Tell, Don’t Ask
- Law of Demeter — Don’t Talk to Strangers
- CQS — Don’t Mix Queries and Commands
- The Common Direction of All Three Principles
TDA — Tell, Don’t Ask
Getter Addiction
TDA (Tell, Don’t Ask) is exactly what the name says. Instead of asking an object for its state and having the caller make decisions, tell the object to perform the action.
The most common TDA violation in production code is getter abuse. Take a look at the following code.
This is logic for applying a discount to an order.
public class OrderService {
public void applyDiscount(Order order) {
if (order.getTotalPrice() > 100_000) {
double discounted = order.getTotalPrice() * 0.9;
order.setTotalPrice(discounted);
}
}
}
Order’s state is extracted (getTotalPrice), OrderService makes the decision (> 100_000), and the value is pushed back in (setTotalPrice). Order has been reduced to nothing more than a data container. When this pattern spreads, business logic scatters across the service layer, and Order becomes a blob of getters and setters.
Delegating Behavior Changes Everything
Let’s rewrite the same feature the TDA way.
Move discount evaluation and application inside Order.
public class Order {
private double totalPrice;
public void applyDiscountIfEligible() {
if (this.totalPrice > 100_000) {
this.totalPrice *= 0.9;
}
}
}
The calling side becomes simple.
public class OrderService {
public void applyDiscount(Order order) {
order.applyDiscountIfEligible();
}
}
OrderService doesn’t need to know the discount conditions. It just tells Order to “apply the discount.” Even if the discount threshold changes from 100,000 to 50,000, only Order’s internals need to be modified.
How the Dependency Flow Changes
Comparing the dependency flow before and after TDA makes the difference clear.
flowchart LR
subgraph before["Before TDA — Extract state and decide"]
direction LR
SVC1["OrderService"] -->|"getTotalPrice()"| ORD1["Order"]
SVC1 -->|"setTotalPrice()"| ORD1
SVC1 -.-|"Discount logic<br/>lives in Service"| SVC1
end
Before applying TDA, OrderService must know Order’s internal state. It extracts via getter, evaluates the condition, and pushes back via setter. The service is deeply coupled to the domain object’s details.
flowchart LR
subgraph after["After TDA — Delegate behavior"]
direction LR
SVC2["OrderService"] -->|"applyDiscountIfEligible()"| ORD2["Order"]
ORD2 -.-|"Discount logic<br/>encapsulated in Order"| ORD2
end
After applying TDA, the message OrderService sends is reduced to one. Even if Order’s internal structure changes, the service code remains unaffected.
A More Complex Example — Payment Validation
Let’s look at a situation closer to production. This code checks membership grade and balance before processing a payment.
The approach of extracting state via getters and making decisions.
public class PaymentService {
public void processPayment(Member member, int amount) {
if (member.getGrade() == Grade.BLOCKED) {
throw new IllegalStateException("Blocked member");
}
if (member.getBalance() < amount) {
throw new IllegalStateException("Insufficient balance");
}
member.setBalance(member.getBalance() - amount);
}
}
PaymentService knows everything about Member’s grade system and balance structure. If any field in Member changes, this service must change too.
Applying TDA, we delegate payment eligibility checks and balance deduction to Member.
public class Member {
private Grade grade;
private int balance;
public void pay(int amount) {
if (this.grade == Grade.BLOCKED) {
throw new IllegalStateException("Blocked member");
}
if (this.balance < amount) {
throw new IllegalStateException("Insufficient balance");
}
this.balance -= amount;
}
}
The service becomes a one-liner.
public class PaymentService {
public void processPayment(Member member, int amount) {
member.pay(amount);
}
}
Business rules are now cohesive within Member. Even if the grade system is refined into BLOCKED, SUSPENDED, and ACTIVE, PaymentService needs no changes.
The Essence of TDA
TDA in one sentence: Instead of asking an object for its state and having the caller decide, delegate both the decision and the action to the object. This is also the essence of encapsulation. Data and the logic that operates on that data should live in the same place.
That said, not all getters are bad. Reading state to pass data to a view or for logging is natural. The problem is the pattern of extracting state via getters and then having the caller make business decisions. When you spot this pattern, suspect a TDA violation.
Law of Demeter — Don’t Talk to Strangers
The Real Meaning of “Only Use One Dot”
The Law of Demeter (LoD) is a design principle named after the Demeter Project at Northeastern University in 1987. The core idea is simple: An object should only send messages to its immediate friends.
More formally, the objects that a method M can send messages to are limited to:
- The object itself that
Mbelongs to (this) - Objects passed as parameters to
M - Objects created directly within
M - Fields of the object that
Mbelongs to
The classic code that violates this rule is method chaining.
A typical Law of Demeter violation.
// Get the customer's city name
String city = customer.getAddress().getCity().getName();
customer is an immediate friend. But the Address returned by getAddress()? That’s a friend of a friend. The City returned by getCity()? A friend of a friend of a friend. You’re traversing the internals of unfamiliar objects in a chain.
This is where the maxim “Only use one dot” comes from. But this maxim shouldn’t be taken literally. The criterion isn’t the number of dots (.) but how much of an object’s internal structure you’re exposing.
This code has multiple dots but is not a Law of Demeter violation.
String result = "hello"
.trim()
.toUpperCase()
.substring(0, 3);
String methods return a new String each time. You’re not traversing internal structure — you’re chaining operations on the same type. This is called method chaining or fluent interface. Java’s Stream API and Kotlin’s scope functions work the same way.
The Structure of the Violation
The dependency structure created by a Law of Demeter violation becomes clear in a diagram.
flowchart LR
subgraph violation["Law of Demeter Violation — Traversing internal structure"]
direction LR
SVC["Service"] -->|"getAddress()"| CUST["Customer"]
SVC -->|"getCity()"| ADDR["Address"]
SVC -->|"getName()"| CITY["City"]
end
style ADDR fill:#ff6b6b,color:#fff
style CITY fill:#ff6b6b,color:#fff
Service knows the structure of all three objects: Customer, Address, and City. If Address’s internal structure changes? If City’s name field is renamed to label? Service breaks. The blast radius of change is wide.
A Law of Demeter-compliant structure is different.
flowchart LR
subgraph compliant["Law of Demeter Compliant — Only talk to immediate friends"]
direction LR
SVC2["Service"] -->|"getCityName()"| CUST2["Customer"]
CUST2 -->|"Delegates internally"| ADDR2["Address"]
ADDR2 -->|"Delegates internally"| CITY2["City"]
end
style ADDR2 fill:#51cf66,color:#fff
style CITY2 fill:#51cf66,color:#fff
Service only knows Customer. It doesn’t need to know that Address and City even exist.
Refactoring
The fix for the violation is to create delegation methods.
Add a delegation method to Customer.
public class Customer {
private Address address;
public String getCityName() {
return address.getCityName();
}
}
public class Address {
private City city;
public String getCityName() {
return city.getName();
}
}
The calling side becomes clean.
String city = customer.getCityName();
One dot, and Service doesn’t know Customer’s internal structure. Whether Address changes to Region or City gets a new code field, Service is unaffected.
The Cost of Delegation Methods
The counterargument is: won’t delegation methods just pile up? It’s a fair point. Customer.getCityName(), Customer.getZipCode(), Customer.getStreet() — if ten or twenty of these accumulate, Customer becomes bloated.
When this happens, check two things.
First, does the caller really need that data? Think about why you’re extracting the city name. If it’s to calculate shipping costs, customer.calculateShippingFee() might be a better design. This connects with TDA.
Second, is the responsibility distribution wrong? If too much information is concentrated in Customer, consider splitting from an SRP perspective.
The Law of Demeter isn’t a rule to follow blindly. It’s a warning light that change becomes difficult when knowledge of internal structure spreads. Judge not by the number of dots, but by the blast radius of change.
CQS — Don’t Mix Queries and Commands
One Method, Two Roles
CQS (Command-Query Separation) was proposed by Bertrand Meyer in Object-Oriented Software Construction. The rule is straightforward.
Every method should be either a Command or a Query. It should not be both.
- Command: Changes state. Returns nothing (
void) - Query: Returns a value. Does not change state (no side effects)
Why separate them? Because predictability changes. Queries return the same result no matter how many times they’re called, and system state doesn’t change. They’re safe to call. Commands change state, so you must be careful about call order and frequency.
When the two are mixed, problems arise.
A Stack pop method that violates CQS.
public class Stack<T> {
private final List<T> elements = new ArrayList<>();
// Command + Query combined
public T pop() {
if (elements.isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size() - 1); // State change + value return
}
}
pop() returns the top element while simultaneously removing it. A classic CQS violation where query and command are merged into one. You asked “what’s on top?” but the act of asking changes the state.
What Happens When You Separate Them
Apply CQS to split command and query.
public class Stack<T> {
private final List<T> elements = new ArrayList<>();
// Query — Returns a value without changing state
public T peek() {
if (elements.isEmpty()) {
throw new EmptyStackException();
}
return elements.get(elements.size() - 1);
}
// Command — Changes state with no return value
public void remove() {
if (elements.isEmpty()) {
throw new EmptyStackException();
}
elements.remove(elements.size() - 1);
}
}
peek() can be called any number of times without changing the stack. remove() has no return value, so just from the signature you can predict “this will change state.”
The intent becomes clear on the calling side.
T top = stack.peek(); // No state change — safe to call
doSomething(top);
stack.remove(); // Explicitly changes state
CQS Violations You’ll Encounter in Practice
Stack.pop() is a textbook example. Let’s look at violation patterns you’ll encounter more often in practice.
A method that saves and returns an ID.
public class UserRepository {
public Long save(User user) {
// INSERT into DB (state change)
// Return generated ID (query)
return generatedId;
}
}
save() is both a command and a query. Strictly following CQS, save() should return void, and if you need the ID, you should query it separately. But in this case, strict CQS is impractical. Needing the ID right after an INSERT is an extremely common pattern.
Meyer himself acknowledged this. CQS is a principle, not an absolute law. What matters is that the caller can clearly tell “does this method change state or not?”
Here’s another pattern common in concurrent environments.
putIfAbsent inserts if absent, or returns the existing value.
V existingValue = map.putIfAbsent(key, newValue);
This is technically a CQS violation too, but it’s intentionally combined to guarantee atomicity. If you separate command and query, another thread could intervene between them.
CQS and CQRS
CQRS (Command-Query Responsibility Segregation), which has a similar name to CQS, is a pattern that extends CQS to the architectural level. While CQS separates commands and queries at the method level, CQRS separates write and read models at the system level. It might use separate databases for writing and reading.
Understanding CQS makes it easier to grasp CQRS’s motivation. The benefits of separating queries and commands at the method level — predictability, testability, simplified reasoning — are elevated to the entire system.
The Common Direction of All Three Principles
TDA, Law of Demeter, CQS. Different starting points and different emphasis, but there’s an area where all three overlap.
| Principle | Core Message | What It Prohibits |
|---|---|---|
| TDA | Don’t ask for state — tell the object what to do | The pattern of extracting via getter and having the caller decide |
| Law of Demeter | Only talk to your immediate friends | Method chains that traverse internal object structure |
| CQS | Don’t mix queries and commands | Combining state change and value return in a single method |
The common direction is respecting object autonomy. Each object should have authority over its own data, the outside world shouldn’t peek at internals, and the intent of messages should be clear.
These principles naturally connect with SOLID.
- TDA aligns with SRP. Business logic coheres in domain objects instead of scattering across services
- The Law of Demeter connects with ISP. It minimizes the interfaces an object needs to know
- CQS supports OCP. When queries work without side effects, it’s easy to compose queries to extend functionality
Ultimately, the aim of good object-oriented design is the same: Keep coupling loose between objects while making each object’s responsibilities clear. If SOLID builds the structural skeleton, TDA, the Law of Demeter, and CQS refine how objects converse on top of that skeleton.
In Kotlin, these principles are well-supported at the language level. Data class copy() expresses state changes while maintaining immutability, extension functions provide a rich API while respecting the Law of Demeter, and the distinction between val and var aligns with CQS thinking.
Here’s a concise Kotlin summary of TDA, the Law of Demeter, and CQS.
// TDA — Delegate the behavior
class Order(private var totalPrice: Long) {
fun applyDiscountIfEligible() {
if (totalPrice > 100_000) totalPrice = (totalPrice * 0.9).toLong()
}
}
// Law of Demeter — Only talk to friends
class Customer(private val address: Address) {
fun getCityName(): String = address.getCityName()
}
// CQS — Separate queries from commands
class Wallet(private var balance: Long) {
fun getBalance(): Long = balance // Query
fun withdraw(amount: Long) { // Command
require(balance >= amount) { "Insufficient balance" }
balance -= amount
}
}
In the next part, we’ll cover the final topic of this series: inheritance vs. composition. We’ll explore why GoF advised “favor composition over inheritance” and when inheritance is truly appropriate.




Loading comments...