Rich vs Anemic Domain Model
Fast recall
Quick revision
Core answer
Why putting business logic in service methods (anemic) leads to inconsistent state and scattered rules, and how rich models prevent that by encapsulating invariants.
Defending anemic models because 'JPA needs setters' — it doesn't (use protected setters or builder); or building 'rich' models that just expose more methods on a still-anemic object.
Discuss tell-don't-ask, encapsulation invariants in constructors, value objects, and how rich models map cleanly to DDD aggregates.
Interview-ready guide
Interview answer
The two models side by side
Anemic: Order has only getters/setters; OrderService has all the logic. orderService.cancel(o) checks status, mutates fields. Order can be put into invalid state from anywhere because setters are exposed.
Rich: Order is constructed valid (invariants in constructor), exposes public business methods (cancel(), addItem()), no setters. order.total cannot be set wrong because there's no setter — it's recomputed from items every time.
Why anemic hurts at scale
- Scattered business rules: 'cannot cancel shipped' lives in OrderService.cancel, RefundService.refund, AdminCancelService.adminCancel. Three places, three risks of drift
- Invalid state reachable: setStatus(SHIPPED) then setTotal(0) produces meaningless object
- Tell-don't-ask lost: every caller asks for data, decides outside
- Service classes balloon: OrderService becomes 2000 lines
- Hard to test in isolation
- Duplicated derived state: total cached in entity also recalculated in services, drifts in production
What rich models give you
- Invariants in constructor: object cannot exist in invalid state. If it's constructed, it's valid. Most powerful OOP guarantee
- Self-documenting: order.cancel() obvious, orderService.cancel(order) doesn't say where the rules live
- Centralized logic: change once, applies everywhere
- Composable: order.cancel(); refunds.process(order); reads like the use case
- Easier testing: plain JUnit on the entity
- Aligns with DDD aggregates
JPA doesn't force anemic
Myth: 'JPA needs public setters; therefore entities must be anemic.'
Reality: JPA needs a way to set fields — either field access (no setter) or protected setters. Protected keeps callers out while letting JPA reflect. Records work with Hibernate 6+ for non-mutable value objects.
Even cleaner: separate domain model from JPA entity (Clean Architecture style) and avoid the conflict.
When anemic is actually fine
- DTOs: transfer data across boundaries. Pure data, no behavior. Records
- Read models / projections: built from events for queries. Pure data; query API is the behavior
- Trivial CRUD entities like Country lookup table
- Configuration / metadata objects
Smell isn't 'class without behavior' — it's 'class with behavior elsewhere that should be on it.'
Tell-don't-ask heuristic
Asking (anemic): caller checks getStatus, decides, calls setStatus. Rules outside the object. Telling (rich): order.submit(); validates internally, throws if invalid. Rules live with data.
If services are full of 'if entity.getX == ... entity.setY' blocks, entity is anemic and service is doing the entity's job.
Common mistakes
- Calling something rich because it has methods — isShipped() that just returns status==SHIPPED is still anemic
- Using setters from anywhere — even with cancel(), leaving setStatus public lets callers bypass invariants
- Splitting one aggregate's logic across multiple objects (Order + OrderHelper + OrderValidator) — defeats encapsulation
- Persistence concerns in business methods — order.cancel() shouldn't call orderRepo.save
- Treating value objects as too small to bother — Money(amount, currency) preventing USD+EUR addition is the textbook win
- Refactoring to rich without DDD intent — just renaming doesn't fix poorly modeled aggregates
Recommendation
Build rich domain models for entities and aggregates that have real business behavior — enforce invariants in constructors, use named business methods (order.cancel(), order.submit()), avoid public setters, and recompute derived state inside the entity. Use protected setters or field access for JPA when the entity is also a JPA entity, or separate domain entities from JPA entities entirely (Clean Architecture style). Anemic is fine for DTOs, read models, and trivial lookup tables — not everything needs behavior. The litmus test: 'tell, don't ask.' If your services are full of 'if entity.getX...' branches mutating the entity, your entity is anemic.
Rich vs Anemic Domain Model
Direct Answer
An anemic domain model has data classes (just getters/setters) and all business logic in service classes — the objects don't enforce their own invariants, and the same logic is often duplicated or scattered. A rich domain model has objects that own both data and the behavior that operates on it — invariants enforced inside the entity, validation in the constructor, business operations as methods on the object. Rich models reduce bugs from invalid state, make the code self-documenting, and pair naturally with DDD; anemic models are the default in many Spring CRUD apps and become unmanageable as business logic grows.
Common Mistakes & Pitfalls
Defending anemic models because 'JPA needs setters' — it doesn't (use protected setters or builder); or building 'rich' models that just expose more methods on a still-anemic object.
Check your understanding
Answer these without looking back at the article.
- Walk through refactoring an anemic Order into a rich Order — what's your first move?
- How do you handle JPA's need for setters in a rich model?
- Can a rich domain model coexist with Spring Data JPA?
- When is anemic the honest choice?
Related interview prep categories
1Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.