skip to content

How do the "feature envy" code smell and the "anemic domain model" relate to Tell, Don't Ask, and how would you refactor a case of each?

level: middleimportance: must knowfreq 48%

answer

  1. envy: method uses another class's data most
  2. anemic: fields+accessors, logic in services
  3. Extract Method -> Move Method
  4. service keeps IO, transactions, orchestration
  5. DTOs/mappers/read models are exempt

basics

~20 s

Both are what ask-style code looks like at scale. Feature envy is one method using another object's data more than its own; anemic model is data-only classes with all logic in services. Fix by moving the logic onto the class that owns the data.

solid answer

~50 s

Feature envy: a method that reaches into another object's accessors repeatedly - it "envies" that object's data and belongs on it. The refactoring is Move Method (or Extract Method then Move Method) so the logic sits with the fields it reads. Anemic domain model: domain classes reduced to fields plus getters/setters, with every rule in a procedural service layer - the systemic version of the same smell, since every service call is an ask. The refactoring is to migrate behavior class by class: find each `if (entity.getX()) entity.setY()` block in the service, name the operation in domain terms, move it onto the entity, tighten or delete the now-unused setters, and leave the service as a thin coordinator handling transactions, IO and multi-object orchestration. Both are heuristics, not verdicts: mappers, DTOs, and legitimately cross-entity policies are supposed to look this way.

code

pseudocode · 18 lines
pseudocode
// Envious service method (anemic Order)
class OrderService {
  void submit(Order o) {
    if (o.getStatus() != DRAFT) throw new IllegalState();
    if (o.getLines().isEmpty()) throw new IllegalState();
    o.setStatus(SUBMITTED); o.setSubmittedAt(now());
  }
}

// After Move Method: rule owned by the data
class Order {
  void submit(Clock clock) {
    require(status == DRAFT, "already submitted");
    require(!lines.isEmpty(), "empty order");
    status = SUBMITTED; submittedAt = clock.now();
  }
}
class OrderService { void submit(id) { tx { repo.load(id).submit(clock); } } }

go deeper

for a junior

Define both smells and name Move Method as the fix; give the order-submission example.

for a middle

Show the mechanical refactoring steps, say what stays in the service (IO, transactions, orchestration), and name at least two legitimate exceptions.

for a senior

Discuss incremental migration strategy, the multi-owner case, and when anemic is deliberately correct (CRUD, read models, CQRS query side).

for a principal

Frame it as where invariants are enforced in the architecture: write model owns rules, read model stays anemic; discuss team-scale consequences when domain rules leak into many services.

## The two smells **Feature envy** (Fowler's catalogue): a method is more interested in the data of some *other* class than of the class it lives in. Detection: count accessor calls in the method body per target. If most reads are `other.getA()`, `other.getB()`, `other.getC()`, the method is envious of `other`. ``` // in ShippingCalculator double fee(Order order) { double w = order.getWeight(); Address a = order.getAddress(); if (order.getLines().size() > 10) w *= 0.9; return rate(a.getZone()) * w; } ``` Everything read belongs to `Order` (and `Address`). This is ask-style at method scale. **Anemic domain model** (Fowler's term, deliberately pejorative): domain classes contain only state and accessors; all behavior lives in a separate service/manager/helper layer. It looks object-oriented (there are classes named after the domain) but is procedural - data structures plus transaction scripts. Ask-style at *architecture* scale. ## Why they are the same disease Tell, Don't Ask says a decision that depends on an object's data belongs to that object. Feature envy is one violation; anemic model is the policy of violating it everywhere. Consequences are identical, only the blast radius differs: - **Rule duplication.** Two services both need "is this order submittable?"; the two copies drift. - **Unenforceable invariants.** Public setters mean any code path can produce an illegal object; validation becomes a convention nobody can guarantee. - **Representation lock-in.** Every field is effectively public API; changing `List<Line>` to a value object breaks all callers. - **Low cohesion / shotgun surgery.** One domain change touches the entity, several services and their tests. ## Refactoring feature envy 1. **Extract Method** so the envious logic is a whole method by itself. 2. **Move Method** onto the class whose data it uses; parameters that came from that class disappear. 3. Delete accessors that now have no callers. ``` // after class Order { Money shippingFee(RateTable rates) { ... uses own fields ... } } ``` If the method needs data from *two* classes roughly equally, move it to the one that changes for the same reasons, or keep it in a domain service that both are passed to - envy split across two owners is not automatically fixable by moving. ## Migrating an anemic model (incrementally, not by rewrite) 1. Pick one aggregate/entity with real rules. 2. In the service, find each cluster of `entity.getX()`/`entity.setY()`. 3. Name it in the business language (`submit`, `cancel`, `applyDiscount`) - if you cannot name it, it may genuinely be application logic, not domain logic. 4. Move it onto the entity; the service now calls one method. 5. Narrow the constructor to only legal states, replace setters with intention-revealing methods, then delete unused accessors. 6. Keep in the service: transaction demarcation, repository/IO calls, authorization, mapping to DTOs, orchestration across aggregates, and calls to external systems. ## Legitimate exceptions - do not "fix" these - **DTOs, API payloads, events, config records**: data-only by contract; behavior there would couple the wire format to the domain. - **Mappers, serializers, presenters, report builders**: their job is to read foreign data; the envy is the point. - **Cross-entity policies** (pricing over customer + order + campaign): put them in a domain service or policy object; forcing them onto one entity creates the god object the principle is supposed to prevent. - **Read models / CQRS query side**: projections are deliberately dumb data. - **Frameworks that require accessors** (ORMs, bean binding): satisfy them with field access, package-private setters, or a persistence-model mapping rather than opening the domain. ## Tooling Static analyzers can flag feature envy heuristically (e.g. via LCOM/cohesion or 'accesses foreign data' rules), and 'law of Demeter'/'data class' inspections exist in common IDEs and linters. Treat the output as a candidate list, not a defect list.

  • Is an anemic domain model always wrong?
    No. For thin CRUD, reporting, ETL, and read-side projections there may be no invariants worth protecting, and transaction scripts over data structures are simpler and cheaper. The smell matters when there is real domain complexity - rules, state machines, invariants - because that is when duplicated and unenforceable logic starts to cost.
  • A method uses data from two objects about equally. Where does it go?
    Neither move fully removes the envy, so choose by change reason: put it where the rule is most likely to evolve, or introduce a domain service/policy object that receives both. Alternatively introduce a new value object representing the concept the two fields form together, and give the behavior to it.

Feature envy is one employee constantly rummaging in a colleague's filing cabinet to finish a task. An anemic model is a whole company where filing clerks only hold folders and every decision is made by a single manager reading them aloud - correct, but the clerks can never guarantee anything about their own folders.

saying these in an interview costs you the question

  • Treating every service class as anemic-by-definition and pushing IO, transactions or authorization into entities
  • Moving envious code onto DTOs, events or mappers, which is exactly where data-only is correct
  • Assuming 'add behavior to entities' means big-bang rewriting the domain instead of incremental Move Method
  • Claiming an anemic model is fine because 'the service layer validates' - validation scattered across callers is precisely the unenforceable-invariant problem
  • Confusing feature envy with a simple long method; envy is about whose data is being used, not length

context