The Anti-Pattern
An anemic domain model is a class that holds data and does nothing else: public getters, public setters, zero behavior. All real logic migrates to XxxService classes that reach into the data holders, extract fields, run rules, and write results back. Fowler named it an anti-pattern — because it quietly reverses encapsulation:
ANEMIC (procedural OO) RICH (objects own behavior)
┌──────────────┐ ┌────────────────────────────┐
│ Order │ │ Order │
│ - items │ no methods beyond │ - items │
│ - status │ accessors │ + ship() validates & │
│ - total │ │ + cancel() transitions │
└──────────────┘ │ + addItem() state here │
└────────────────────────────┘
OrderService.ship(order): order.ship();
if (order.getStatus()!=PENDING)
throw ...; invariants enforced INSIDE the
order.setStatus(SHIPPED); object — impossible to bypass
any code anywhere can do
order.setStatus(SHIPPED) ← the hole
Why It Happens Anyway
- ORM/JSON tooling culture: Hibernate entities and DTO shapes get generated as naked field bags; the habit leaks into domain classes.
- Layered-architecture dogma: “keep services fat, models dumb” misreads layering as logic eviction.
- JavaBean conventions: frameworks rewarded getter/setter shapes so thoroughly they became identity.
- Team scale-up: procedural service code needs less OO fluency than responsibility assignment.
The Concrete Damage
| Damage | Mechanism |
|---|---|
| Broken invariants | Any caller can set status = SHIPPED without payment; validity depends on everyone’s discipline |
| Scattered rules | ”Can cancel?” answered differently in three services; bugs hide in divergence |
| Weak expressiveness | The model documents data, not behavior — reading it teaches nothing about business rules |
| Test bloat | Testing rules requires full service wiring instead of constructing one object |
The Fix — Move Behavior Home
class Order {
private Status status = Status.PENDING;
private final List<Item> items = new ArrayList<>();
public void ship() {
requireStatus(Status.PAID); // invariant lives with state
this.status = Status.SHIPPED;
}
public void cancel() {
if (status == Status.SHIPPED || status == Status.DELIVERED)
throw new IllegalStateException("cannot cancel after shipping");
this.status = Status.CANCELLED;
}
}
Services remain — for orchestration: transactions, external calls, coordinating several aggregates. Business rules about one concept’s state belong in that concept.
Boundary Nuance
Not every class deserves behavior. DTOs, request/response records, config objects are legitimately anemic — they exist to cross boundaries. The anti-pattern is applying data-holder shape to domain concepts whose whole job is enforcing rules (Order, Account, Reservation).
Interview Framing
Asked to review “a model with services holding all logic”: name the anemia, show one rule moving into the object, and state the orchestration-vs-rules boundary — that last distinction is what separates memorized DDD vocabulary from judgment.
Premium Content
Unlock Anemic Domain Model and all premium lessons with a subscription.
From ₹199.99/year — See plans