Feature Envy: Move Behavior Closer to the Data It Uses

A method can live in one class while spending most of its time inspecting another. It asks that other object for several values, combines them according to rules about that object, and perhaps repeats the same pattern elsewhere. The code works, but changing the data often means hunting down behavior in unrelated places.

This is the design smell commonly called feature envy. The name matters less than the question behind it: does this behavior belong closer to the data and rules it depends on?

This article shows how to answer that question without turning “move methods to the data” into a rigid rule. You will learn how to spot feature envy, refactor it safely, and recognize cases where the behavior should stay where it is.

Start with dependency, not file location

A method belongs where its responsibility can be understood and changed with the fewest unnecessary dependencies. One useful signal is which object’s information the method needs to do its job.

Consider a checkout service that calculates the value of an order line:

class CheckoutService:
    lineTotal(line):
        subtotal = line.product.unitPrice * line.quantity
        discount = subtotal * line.product.discountRate
        return subtotal - discount

lineTotal is physically inside CheckoutService, but almost every fact it uses comes from line and line.product. More importantly, the calculation expresses rules about an order line: its quantity, price, and applicable discount.

That mismatch is the useful part of feature envy. The method’s location says “checkout coordination,” while its dependencies say “order-line calculation.”

A simple mental model is:

where behavior lives
        |
        v
what information does it repeatedly pull from elsewhere?
        |
        v
where do those rules naturally change?

Counting field accesses can reveal a candidate, but it doesn’t decide the design. The stronger question is where the knowledge belongs.

See the smallest useful refactoring

Suppose OrderLine already owns the quantity and product reference. We can move the calculation there:

class OrderLine:
    total():
        subtotal = product.unitPrice * quantity
        discount = subtotal * product.discountRate
        return subtotal - discount

The checkout code becomes:

total = line.total()

This change does more than shorten the caller. Before the move, CheckoutService needed to know how an order line’s total was derived. After the move, it only needs to know that an order line can calculate its total.

The dependency changes from this:

CheckoutService
  -> OrderLine.quantity
  -> OrderLine.product
  -> Product.unitPrice
  -> Product.discountRate

into this:

CheckoutService -> OrderLine.total()
OrderLine       -> data needed for its calculation

The calculation still has dependencies; they haven’t disappeared. They are now concentrated behind an operation whose name expresses the result the caller needs.

That concentration is valuable when the calculation changes with the order-line rules. If discounts later gain a minimum quantity, code that asks for line.total() doesn’t need to learn the new rule.

Look for knowledge that travels together

Feature envy is easiest to recognize when a method repeatedly asks another object for data and then makes decisions about that data.

For example:

shippingLabel(order):
    if order.destination.country == "...":
        ...
    if order.destination.postalCode == "...":
        ...
    return format(order.destination.street,
                  order.destination.city,
                  order.destination.postalCode)

The exact destination rules are omitted because this is teaching pseudocode. The structural clue is that the method reaches through order into destination several times and interprets destination details itself.

Possible improvements include giving Destination an operation that represents the needed decision, or moving a cohesive part of label formatting to it. The right operation depends on the domain. The goal is not to hide every field behind a method with the same name. It is to place a meaningful rule with the information that gives that rule context.

A particularly strong signal appears when the same group of getters is used together in several callers. Repetition suggests that callers share knowledge that could have one owner. If three services independently know how an order determines whether expedited delivery is allowed, the problem isn’t merely duplicate syntax. The policy has no clear home.

Tell an object what you need instead of reconstructing its rules

Feature envy often appears as procedural code built from getters:

if account.status == ACTIVE and
   account.balance >= amount and
   not account.frozen:
    approveWithdrawal()

A caller that only needs to know whether a withdrawal is allowed now knows three pieces of account state and how they combine.

If that decision is genuinely an account rule, a more focused interface might be:

if account.canWithdraw(amount):
    approveWithdrawal()

Now the caller asks a question in domain terms. If the rule changes, the account owns the change.

There is an important limit here. Replacing getters with arbitrary command methods is not automatically better. account.canWithdraw(amount) should represent a coherent responsibility of the account model. If withdrawal eligibility depends on fraud scoring, a regulatory service, current exchange rates, and a remote risk system, forcing all of that into Account may create a much worse dependency structure.

Move knowledge together when it is cohesive. Do not use feature envy as an excuse to make one object responsible for an entire workflow.

Refactor in small, observable steps

Moving behavior changes structure, and sometimes it changes which object can access which dependency. A controlled sequence makes mistakes easier to detect.

First, identify the smallest cohesive calculation or decision. Avoid moving a large method just because part of it envies another object. Extract the relevant part if necessary.

Next, make the behavior available on the likely owner while keeping the old caller temporarily:

class OrderLine:
    total():
        ...

class CheckoutService:
    lineTotal(line):
        return line.total()

Run the relevant tests or otherwise verify observable behavior. Then replace callers of lineTotal with line.total() where that improves the design. Once the forwarding method has no useful role, remove it.

This sequence separates two concerns: preserving behavior and changing call sites. In a mature codebase, that is often easier to review than a single edit that moves logic, renames concepts, and restructures callers at the same time.

If the original method has no tests and its behavior is difficult to reason about, characterize the current behavior before moving it. Refactoring is safer when you can distinguish an intended structural change from an accidental behavioral one.

Moving a method may reveal the real boundary

Sometimes a method cannot move cleanly because it needs many values from both its current object and the object it envies.

Imagine pricing logic like this:

quotePrice(customer, product):
    base = product.listPrice
    tier = customer.pricingTier
    region = customer.region
    category = product.category
    return applyRules(base, tier, region, category)

Does this belong to Customer because it uses customer information? To Product because it uses product information? Neither answer is obviously correct. The calculation depends materially on both.

That is useful evidence. The behavior may represent a separate concept such as a pricing policy. A dedicated object can receive the inputs it needs:

price = pricingPolicy.quote(customer, product)

This is not a reason to create a new class for every calculation. It is a reminder that feature envy is a diagnostic signal, not a command to move a method to whichever object has the most getters.

When behavior coordinates several peers, depends on external services, or represents a policy independent of any one entity, a separate collaborator can be the clearer home.

Do not confuse data transformation with feature envy

Some code is supposed to inspect data from other objects. Serializers, presenters, report builders, mapping layers, and adapters often exist specifically to transform one representation into another.

A JSON serializer may read many fields from an object:

serialize(customer):
    return {
        "id": customer.id,
        "name": customer.name,
        "status": customer.status
    }

Moving serialize into Customer could couple the domain object to a transport format it otherwise doesn’t need to know about. The high number of field reads is real, but the serializer’s responsibility explains them.

The same caution applies at architectural boundaries. A persistence mapper may inspect an entity because translating between a domain model and storage representation is exactly its job. A UI presenter may combine several values because presentation is its responsibility.

Ask what kind of knowledge the method contains. If it knows domain rules about another object, moving it is worth considering. If it knows how to translate another object into an external representation, keeping that knowledge in a boundary component may preserve a cleaner separation.

Watch for moves that create new problems

A method move can reduce one dependency while introducing another. Check the result rather than assuming relocation improved it.

One warning sign is a method that now needs a long list of parameters because its new owner lacks most of the required context:

order.calculateTotal(taxPolicy, currencyRates, inventory, clock, promotions)

That signature may be telling you that total calculation is not solely an Order responsibility, or that several dependencies should be represented by a cohesive policy object. Passing dependencies explicitly is not inherently wrong, but a move that requires importing an entire workflow into an entity deserves another look.

Another mistake is moving behavior solely to reduce line count in a service. A short service can still be poorly designed, and a large domain object can become a dumping ground. Judge the move by responsibility and change coupling: when a rule changes, are the pieces that must change now easier to find and reason about?

Also avoid exposing more internal state just to make the move possible. If A envies B, adding getters to B for every private detail can deepen the coupling. Prefer an operation that expresses what A needs, provided that operation makes sense as part of B’s responsibility.

Know when the behavior should stay put

Keep the behavior where it is when its current location reflects the real responsibility better than the data location does. Common cases include orchestration across several components, boundary translation, policies that intentionally combine independent objects, and operations whose dependencies are external rather than intrinsic to one domain object.

A small amount of feature envy can also be cheaper than a new abstraction. If a one-line formatter reads two stable fields and has no domain rule, moving it may add indirection without reducing meaningful coupling.

The decision becomes clearer when you ask about future change. Suppose the rule changes from “discount equals rate times subtotal” to a tiered discount based on quantity. Which concept would a developer reasonably inspect first? Which object should guarantee that all callers use the same rule? Those answers are stronger design evidence than a mechanical count of method calls.

Use feature envy as a question about ownership

When you encounter a method that spends most of its effort examining another object, don’t immediately move it. Identify the knowledge inside the method first.

If the method interprets another object’s state according to rules that naturally change with that object, moving the behavior closer to that data can reduce scattered knowledge and give the rule a clear owner. If the method coordinates peers or translates across a boundary, its current separation may be exactly what the design needs.

The practical next step is small: find one method with repeated getter chains and describe, in one sentence, the rule it implements. Then ask which component should own that sentence. That question usually leads to a better refactoring decision than the smell’s name alone.