Feature Envy: when a method spends more time reading another class's data than its own
Fowler's definition (Refactoring 2nd ed., ch. 3): a method that seems more interested in a class other than the one it is in. The method is “envious” of another class's data. The canonical signal is counting field accesses: if a method on class A makes more calls to class B's getters than to A's own fields, it belongs on B.
class Order {
private items: OrderItem[];
private createdAt: Date;
// This method envies Customer's data.
// It accesses zero Order fields and 4 Customer fields.
calculateShippingCost(customer: Customer): number {
const postalCode = customer.getPostalCode(); // Customer field
const tier = customer.getMembershipTier(); // Customer field
const region = customer.getRegion(); // Customer field
const isCommercial = customer.isCommercial(); // Customer field
if (tier === 'GOLD') return 0;
if (isCommercial) return COMMERCIAL_RATES[region];
return RESIDENTIAL_RATES[postalCode.substring(0, 3)];
}
}
// After Move Method: calculateShippingCost lives on Customer
class Customer {
calculateShippingCost(): number {
if (this.membershipTier === 'GOLD') return 0;
if (this.isCommercial) return COMMERCIAL_RATES[this.region];
return RESIDENTIAL_RATES[this.postalCode.substring(0, 3)];
}
}Feature Envy is a coupling smell, and coupling is the single best-studied predictor of defect density in object-oriented code.
Basili, Briand, Melo (IEEE TSE 1996)
The first rigorous validation of Chidamber-Kemerer metrics found that Coupling Between Objects (CBO) was the strongest predictor of defect density in Java systems. A method that accesses another class's data directly is raising the CBO metric of both classes.
Kitchenham et al. (2004)
A systematic review of 28 OO metrics studies found CBO and RFC (Response For a Class) consistently predictive of fault-proneness across languages and study contexts. Feature Envy is a direct generator of both.
Rahman 2025 meta-study
Feature Envy at rho=0.31 across 28 primary studies - the second-strongest single-smell effect size after God Class. The mechanism is what Basili established: envious methods create invisible coupling that breaks when the envied class changes.
The specific mechanism: when Customer adds a new field (say, preferredCurrency), the developer adding it searches Customer for usages and adds the new field logic there. But Order's envious calculateShippingCost is on a different class. The developer does not see it. The envious method ships with stale logic. The defect surfaces in QA or production.
Move Method (Fowler ch. 9) is often the cleanest refactor in the catalog. The steps:
- Identify the class that the envious method accesses most (the “target class”).
- Create a new method on the target class with the same logic, now using
thisinstead of the parameter. - Replace the original method body with a delegation:
return customer.calculateShippingCost(). - Run tests. Update all callers. Remove the delegation once confirmed.
IDE-assisted: JetBrains Refactor → Move. VS Code language servers support Move for typed languages. The move is safe when the method only reads the target class's data; it requires more care when it modifies state.
SonarCloud has a direct Feature Envy rule for Java and C#. ESLint does not have a direct rule but high complexity combined with single-class call concentration is a proxy.
The human signal is faster: a method name that includes another class's name is an almost-certain Feature Envy indicator. Order.calculateCustomerDiscount(), Invoice.getCustomerAddress(), ShippingService.getCustomerTier() - all suspicious on first inspection.
- Cross-bounded-context coordination in DDD. Aggregate roots communicating across contexts legitimately access each other's public interfaces. This is by design, not by accident.
- Serialisation and DTO methods. A mapper that reads Order's fields to produce an OrderDTO is an intentional data-projection, not Feature Envy.
- Framework-enforced patterns. Some frameworks (Rails controllers, Django views) structurally put code that accesses model data in a layer that is not the model itself. The framework's conventions override the smell heuristic.