Feature envy anti-pattern
Learn why methods that obsess over another class's data signal misplaced responsibility, and how Move Method refactoring creates cleaner, more cohesive designs.
TL;DR
- Feature envy occurs when a method in class A spends most of its time accessing fields from class B, a sign the method belongs in B.
- It creates hidden coupling: when B's internal structure changes, the method in A breaks even though A "owns" it.
- The fix is Move Method: relocate the envious logic to the class that owns the data, then call it from A.
- Feature envy is the inverse of an anemic domain model. Both stem from separating behavior from data.
- Cross-cutting concerns (logging, formatting, metrics) that read from many objects are not feature envy.
The Problem
Your e-commerce codebase has an OrderService that calculates totals, discounts, and tax. Every computation reaches into Order to pull item prices, discount codes, and shipping addresses. The Order class itself is a plain data holder with zero behavior.
// OrderService calculates everything using Order's internals
public class OrderService {
public BigDecimal calculateTotal(Order order) {
BigDecimal subtotal = BigDecimal.ZERO;
for (OrderItem item : order.getItems()) {
subtotal = subtotal.add(
item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
}
BigDecimal discount = order.getDiscountCode() != null
? subtotal.multiply(new BigDecimal("0.10"))
: BigDecimal.ZERO;
BigDecimal taxable = subtotal.subtract(discount);
BigDecimal tax = taxable.multiply(new BigDecimal("0.08"));
return taxable.add(tax);
}
public boolean isHighValue(Order order) {
return calculateTotal(order).compareTo(new BigDecimal("500")) > 0;
}
public String getShippingSummary(Order order) {
return order.getItems().size() + " items to "
+ order.getShippingAddress().getCity();
}
}
Every method in OrderService is about Order, not about OrderService. When the discount model changes from a flat 10% to tiered pricing, you update OrderService. When Order.items changes from a List to a Map, you update OrderService. The service is a parasite living off the entity's data.
When a second service needs the same calculation, it copies the logic rather than trusting OrderService to be canonical. Now two classes compute totals independently, and they diverge.
I've seen this exact pattern in a 200-service codebase where six different services each had their own calculateTotal variant, and none agreed on the result.
The service calls seven getters to do work that the entity should own. If any getter signature changes, the service breaks.
Why It Happens
- Anemic domain model habit. Teams trained on "services do logic, entities hold data" write feature-envious code by default. The entity becomes a glorified struct.
- IDE-generated getters. Auto-generating getters for every field invites other classes to reach in. Once getters exist, developers pull data out instead of pushing behavior in.
- Fear of "fat" entities. Developers worry about putting logic in entities because "entities should be simple." But an
Orderthat knows how to calculate its own total is not fat, it is cohesive. A 10-method entity is far healthier than a 10-method service that computes on the entity's behalf. - Gradual accretion. The first method is small. The second copies its pattern. By the tenth method, the service is a 400-line class that only touches foreign data.
Tell, Don't Ask
The Tell Don't Ask principle says: instead of asking an object for data and computing with it, tell the object what to do. order.calculateTotal() tells. orderService.calculateTotal(order) asks. Feature envy is fundamentally a violation of Tell Don't Ask.
How to Detect It
| Signal | What It Means | How to Check |
|---|---|---|
| Method accesses 3+ getters on another object | Logic probably belongs with the data | Count foreign vs. local field access per method |
| Renaming a field in class B breaks method in A | A depends on B's internals | Rename a field and see what breaks |
| Method could move to another class with no changes | The method is in the wrong class | Try the move mentally: does it simplify both classes? |
| Multiple classes compute the same thing from the same entity | Duplicated logic caused by envious access | Search for similar computations across services |
| Entity class has only getters and setters | Anemic model inviting feature envy | Check if entities have behavior methods |
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.