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.
Feature envy is a cohesion smell: a method is defined on one class but spends most of its effort reading another object's data. The method may be correct, yet its location exposes callers to the other class's representation and makes the data owner less meaningful.
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();
}
}
Most methods in OrderService are 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 coupled to the entity's representation.
When another service needs the same calculation, it may copy the getter-heavy code rather than treating one location as canonical. The result is duplicated logic that can drift when the domain rule changes.
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 cohesive when that behavior uses order data. The right question is which class has the information and the reason to change, not whether the method adds a line to the entity. - 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 several getters on another object | Logic may belong with the data owner | Compare foreign vs. local field access and ask whether the method could move |
| 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 SDEpedia Premium.
Related Articles
Learn why a class that knows too much and does too many unrelated things is hard to test, extend, and maintain, and how to break it apart using Single Responsibility and Extract Class.
Learn why separating all business logic from domain objects into service classes produces procedural code disguised as OOP, and how a rich domain model fixes it.
Learn why chaining method calls across object boundaries violates the Law of Demeter, creates brittle coupling, and how delegating methods fix the problem cleanly.