Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Anemic Domain Model
LLD

Anemic Domain Model

Understand the limitations of domain objects that contain mostly data with little or no business behavior.

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

DamageMechanism
Broken invariantsAny 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 expressivenessThe model documents data, not behavior — reading it teaches nothing about business rules
Test bloatTesting 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.

My Private Notes

Notes are auto-saved locally to this device.