Feature Envy occurs when a method uses data from another object more than its own. It’s a sign that the method probably belongs on that other object instead. The fix is simple: move the method to where the data lives.

Spotting Feature Envy

// ❌ This method is envious of Order's data
class InvoiceGenerator {
  generateInvoice(order: Order): Invoice {
    const subtotal = order.items.reduce(
      (sum, item) => sum + item.price * item.quantity, 0
    );
    const discount = order.customer.loyaltyTier === 'gold'
      ? subtotal * 0.1
      : order.customer.loyaltyTier === 'silver'
        ? subtotal * 0.05
        : 0;
    const tax = (subtotal - discount) * order.taxRate;
    const shipping = order.items.reduce(
      (sum, item) => sum + item.weight, 0
    ) > 10 ? 15.99 : 5.99;
    
    return {
      subtotal,
      discount,
      tax,
      shipping,
      total: subtotal - discount + tax + shipping,
    };
  }
}

This method reaches deep into Order, Customer, and Item objects. It knows their internal structure intimately.

The Fix: Move Behavior to the Data Owner

// ✅ Each object calculates what it knows about
class Order {
  constructor(
    public items: OrderItem[],
    public customer: Customer,
    public taxRate: number,
  ) {}

  get subtotal(): number {
    return this.items.reduce((sum, item) => sum + item.lineTotal, 0);
  }

  get totalWeight(): number {
    return this.items.reduce((sum, item) => sum + item.weight, 0);
  }

  get discount(): number {
    return this.customer.calculateDiscount(this.subtotal);
  }

  get shippingCost(): number {
    return this.totalWeight > 10 ? 15.99 : 5.99;
  }

  get tax(): number {
    return (this.subtotal - this.discount) * this.taxRate;
  }

  get total(): number {
    return this.subtotal - this.discount + this.tax + this.shippingCost;
  }
}

class OrderItem {
  constructor(
    public price: number,
    public quantity: number,
    public weight: number,
  ) {}

  get lineTotal(): number {
    return this.price * this.quantity;
  }
}

class Customer {
  constructor(public loyaltyTier: 'bronze' | 'silver' | 'gold') {}

  calculateDiscount(amount: number): number {
    const rates: Record<string, number> = { gold: 0.1, silver: 0.05, bronze: 0 };
    return amount * (rates[this.loyaltyTier] ?? 0);
  }
}

// Now invoice generation is trivial
class InvoiceGenerator {
  generateInvoice(order: Order): Invoice {
    return {
      subtotal: order.subtotal,
      discount: order.discount,
      tax: order.tax,
      shipping: order.shippingCost,
      total: order.total,
    };
  }
}

Recognizing the Pattern

Feature Envy shows up when you see:

  • Lots of getter calls on another object: obj.getX(), obj.getY(), obj.getZ()
  • Chained property access: order.customer.address.city
  • Calculations using another object’s fields more than your own
  • The same data access pattern repeated across multiple methods

When Feature Envy Is Acceptable

Sometimes a method legitimately needs data from multiple objects. Mappers, formatters, and presenters are valid exceptions:

// This is OK — its job IS to read from multiple sources
class OrderPresenter {
  format(order: Order, locale: string): OrderView {
    return {
      id: order.id,
      date: formatDate(order.createdAt, locale),
      total: formatCurrency(order.total, locale),
      customerName: order.customer.name,
      items: order.items.map(i => ({
        name: i.product.name,
        qty: i.quantity,
        price: formatCurrency(i.lineTotal, locale),
      })),
    };
  }
}

The key question: Is this method’s job to transform/present data from elsewhere? If yes, feature envy is expected. If the method is doing business logic with another object’s data, move it.

“Put things together that change together. Separate things that change for different reasons.” — Robert C. Martin