A method can live in one class while doing most of its work with another class’s data. At first this may seem harmless: the code runs, the names are clear, and the calculation is short. Over time, however, the method often becomes a second place that knows how the other object works.

That design smell is commonly called feature envy. A piece of behavior appears to “envy” another object’s features because it reads that object’s state or calls its methods much more than it uses its own.

The useful lesson is not “move every method that touches another object.” It is: when behavior depends strongly on another object’s data and rules, ask whether that object should own the behavior instead.

This article develops that decision from a small example, then shows when moving behavior improves maintainability and when it would make the design worse.

Start with a calculation in the wrong place

Imagine an order service that calculates the value of a line item:

lineTotal(line):
    product = line.product
    base = product.unitPrice * line.quantity

    if product.taxable:
        return base + base * product.taxRate

    return base

The method belongs to the order service, but look at the knowledge it contains. It knows that a product has a unit price, whether the product is taxable, and which tax rate applies. The service is not merely coordinating an order. It is interpreting product data to implement a product-related pricing rule.

Now suppose the tax rule changes. Taxable products must use a category-specific rate. The product model changes, and this service must change too because it duplicated knowledge about how product pricing works.

That is the maintenance cost behind feature envy: a rule and the information needed to apply it are separated, so one conceptual change can require edits in multiple places.

Use knowledge as the mental model

Do not count field accesses mechanically. Instead, ask what the code needs to know to do its job.

Consider these two methods:

sendReceipt(order):
    email.send(order.customerEmail, renderReceipt(order))

and:

lineTotal(line):
    product = line.product
    base = product.unitPrice * line.quantity
    if product.taxable:
        return base + base * product.taxRate
    return base

Both methods use another object. Only the second one clearly contains detailed knowledge about that object’s rules.

sendReceipt is orchestration. Its purpose is to connect an order, a renderer, and an email capability. Moving email delivery into the order merely because the method reads order.customerEmail would mix unrelated responsibilities.

lineTotal, by contrast, interprets product pricing information. The stronger question is therefore:

If this rule changes, which concept should naturally be responsible for changing with it?

That question is more useful than asking which class currently contains the method.

Move the smallest coherent rule

Suppose the product can calculate the price for a quantity:

product.priceFor(quantity):
    base = unitPrice * quantity

    if taxable:
        return base + base * taxRate

    return base

The order-side code becomes:

lineTotal(line):
    return line.product.priceFor(line.quantity)

The important change is not the shorter caller. The pricing rule now lives beside the product information it interprets.

If the tax representation changes later, callers can continue asking the product for a price instead of learning the new representation. The product exposes an operation that reflects what callers need, while keeping the details of the rule behind that operation.

This is a form of information hiding: callers depend on a useful capability rather than on the internal pieces used to implement that capability.

Why the move can reduce change propagation

Imagine the original design has five callers that calculate taxable prices themselves:

checkout      -> reads unitPrice, taxable, taxRate
invoice       -> reads unitPrice, taxable, taxRate
quote         -> reads unitPrice, taxable, taxRate
admin preview -> reads unitPrice, taxable, taxRate
renewal       -> reads unitPrice, taxable, taxRate

A change from one tax rate to category-based rates can require all five callers to understand the new rule.

After the rule moves behind priceFor, the dependency looks different:

checkout      \
invoice        \
quote           -> product.priceFor(quantity)
admin preview  /
renewal       /

The number of callers has not changed. What changed is what they know. They depend on the pricing operation, not on the ingredients of its implementation.

This distinction matters. Good encapsulation does not eliminate dependencies; it makes dependencies point toward stable, meaningful behavior rather than incidental representation details.

Decide whether the behavior truly belongs there

A move is helpful when the destination owns most of the knowledge required by the behavior. Three questions make that judgment more concrete.

Which data drives the decision?

If a method repeatedly reads several fields from one object and uses them to make domain decisions, that object is a strong candidate for owning the rule.

For example:

isEligibleForPriority(customer):
    return customer.active
       and customer.totalOrders >= 20
       and customer.accountAgeMonths >= 12

If those conditions define what a priority customer means, customer.isEligibleForPriority() may provide a better home. Callers no longer need to know the eligibility formula.

Which concept should change when the rule changes?

Ownership should follow the reason for change, not just data proximity.

Suppose a shipping coordinator reads package.weight and package.destination to choose among several external carriers. The package supplies important data, but carrier selection may depend on contracts, current service availability, or application policy. Moving that entire decision into Package could give the package knowledge it should not have.

In that case, a separate shipping policy may be the better owner even though the policy reads package data.

Does the move improve the public conversation?

Compare:

if account.balance >= amount and not account.frozen:
    account.balance = account.balance - amount

with:

account.withdraw(amount)

The second form gives the caller a domain operation instead of exposing the steps required to perform it. That is useful if Account is responsible for withdrawal rules.

But adding a method such as account.sendMarketingEmail() simply because an email operation needs account data would make the object’s interface less coherent. The method name should represent behavior that naturally belongs to the abstraction.

Move Method is a refactoring, not a redesign shortcut

In an existing codebase, changing ownership is safer when done in small steps.

Assume this method currently belongs to OrderService:

lineTotal(line):
    product = line.product
    base = product.unitPrice * line.quantity
    ...

A practical sequence is:

  1. Ensure tests cover the observable pricing behavior.
  2. Add the equivalent operation to the intended owner.
  3. Make the old method delegate to the new operation.
  4. Move callers gradually if needed.
  5. Remove the old method when nothing uses it.

The temporary delegation is useful because it separates two risks. First you move the rule without changing every caller. Then you simplify callers after the behavior is known to be preserved.

For a small, well-tested local change, doing the move in one edit may be simpler. The staged approach becomes more valuable when the method has many callers or the existing behavior is difficult to understand.

Watch for partial moves

A common mistake is to move the method while leaving the knowledge behind.

For example:

product.priceFor(quantity, taxable, taxRate)

If taxable and taxRate are already properties of the product, passing them back into the product does not improve ownership. The caller still knows which internal values the calculation requires.

Another partial move looks like this:

product.priceFor(quantity):
    return pricingHelper.calculate(
        unitPrice,
        taxable,
        taxRate,
        quantity
    )

This can be perfectly reasonable if pricingHelper represents a genuine pricing policy. But if it is only a procedural container with no independent reason to exist, the rule may still be unnecessarily separated from the concept that owns it.

The goal is not to maximize the number of methods on a data object. The goal is to put each rule where its required knowledge can be managed coherently.

Avoid turning every object into a behavior container

Moving behavior toward data has limits.

Some objects are deliberately simple data carriers. A message received from an external API, a serialization structure, or a read-only projection may exist to transport information rather than enforce domain rules. Adding application behavior to such types can couple transport concerns to domain concerns.

Cross-cutting workflows also need coordination. Completing an order might reserve inventory, charge a payment method, create a shipment, and send a receipt. No single participating object necessarily owns that whole workflow. An application service can coordinate those operations while each participant owns its local rules.

There is also a danger of creating a large object that accumulates every behavior remotely related to its data. If a Customer object starts handling billing, messaging, analytics, permissions, and support workflows, moving methods toward it has reduced one kind of coupling by creating another.

A useful boundary is this: move a rule toward the concept whose invariants and meaning the rule protects, not merely toward whichever object supplies the most fields.

Recognize cases where leaving the method alone is simpler

Feature envy is a diagnostic clue, not a defect that must always be removed.

Leaving behavior where it is can be appropriate when:

  • the method is primarily orchestration across several peers;
  • the destination would gain dependencies that do not belong to it;
  • the source represents a deliberate policy or strategy object;
  • the data object is an external or transport representation;
  • moving the method would make the destination’s interface less coherent;
  • the current code is small, stable, and changing ownership would add indirection without reducing meaningful knowledge duplication.

The decision is about future change cost. If a move makes the rule easier to find, keeps related knowledge together, and reduces the number of places that must understand representation details, it is likely useful. If it only relocates code, it is not much of a refactoring improvement.

Review the design after the move

After moving behavior, inspect both sides.

The old owner should usually know less. If it still reaches into the destination’s fields for the same rule elsewhere, the underlying knowledge duplication remains.

The new owner should become more coherent, not merely larger. Its new method should express behavior that fits the abstraction and should not require a long list of foreign collaborators to work.

Finally, check callers. A successful move often changes their vocabulary from asking for data and reconstructing a decision to asking for the decision directly:

# representation-oriented
if customer.active and customer.totalOrders >= 20:
    ...

# behavior-oriented
if customer.isEligibleForPriority():
    ...

The second form is valuable when eligibility is genuinely a customer rule. The caller states what it needs; the owner decides how that answer is produced.

Conclusion

Feature envy points to a mismatch between where behavior lives and where its required knowledge lives. The smell matters because duplicated knowledge makes changes spread: callers learn internal details, then every representation or rule change has a wider impact.

Use the smell as a question rather than a command. Identify which concept owns the rule, move the smallest coherent behavior toward that owner, and let callers depend on a meaningful operation instead of reconstructing the rule from raw data.

Keep orchestration, external integration, and independent policies separate when they have their own reasons to change. The aim is not to make objects do more. It is to make each important rule have a clear home.