Anemic domain model anti-pattern
Learn why separating all business logic from domain objects into service classes produces procedural code disguised as OOP, and how a rich domain model fixes it.
An anemic domain model keeps state in domain objects but puts the rules that act on that state somewhere else. That separation can be useful at system boundaries, but it becomes an anti-pattern when objects cannot protect their own invariants and every caller must know how to manipulate them safely.
TL;DR
- An anemic domain model has objects that are pure data containers (getters and setters) with all business logic living in service classes that manipulate them from the outside.
- A useful smell: the domain object could be replaced with a plain data structure without losing behavior. If you remove its getters and setters, nothing meaningful is left.
- The fix: move behavior into the domain object. An
Ordershould know how to apply a discount, cancel itself, and calculate its total, not let a service do those things to it from outside. - The key principle is Tell, Don't Ask: tell an object what to do rather than asking for its data and doing the work externally.
The Problem
A typical anemic Order in an enterprise codebase:
// Anemic: a struct with getters and setters, zero behavior
public class Order {
private String id;
private OrderStatus status;
private List<OrderItem> items;
private double discountPercent;
private BigDecimal total;
public String getId() { return id; }
public OrderStatus getStatus() { return status; }
public void setStatus(OrderStatus s) { this.status = s; }
public List<OrderItem> getItems() { return items; }
public double getDiscountPercent() { return discountPercent; }
public void setDiscountPercent(double d) { this.discountPercent = d; }
public BigDecimal getTotal() { return total; }
public void setTotal(BigDecimal t) { this.total = t; }
}
All the business logic lives in a service that reaches into the data bag:
// Service knows all the rules. Order knows nothing.
public class OrderService {
public void applyDiscount(Order order, String code) {
if (order.getStatus() != OrderStatus.PENDING)
throw new IllegalStateException("Order not pending");
Discount d = promotionService.getDiscount(code);
if (d.minOrderValue().compareTo(calculateTotal(order)) > 0)
throw new IllegalStateException("Minimum not met");
order.setDiscountPercent(d.percent());
order.setTotal(calculateTotal(order)
.multiply(BigDecimal.ONE.subtract(
BigDecimal.valueOf(d.percent() / 100.0))));
}
public void cancelOrder(Order order) {
if (order.getStatus() == OrderStatus.SHIPPED)
throw new IllegalStateException("Cannot cancel shipped");
if (order.getStatus() == OrderStatus.DELIVERED)
throw new IllegalStateException("Cannot cancel delivered");
order.setStatus(OrderStatus.CANCELLED);
}
public BigDecimal calculateTotal(Order order) {
return order.getItems().stream()
.map(i -> i.getPrice().multiply(BigDecimal.valueOf(i.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
This is procedural programming with class syntax. Order is a passive data bag, while OrderService owns the knowledge needed to keep the order valid. The class boundary does not protect the order: any caller with a reference can set a status or total directly.
The real damage shows up in testing. To verify that "you cannot apply a discount to a shipped order," you need to instantiate OrderService with all its dependencies (promotionService, etc.) just to test a rule that depends only on Order's status field. With a rich model, the test can create an order, put it in the relevant state, call a domain method, and assert the result without service wiring.
The danger is clear: anyone can call order.setStatus(DELIVERED) directly, bypassing every business rule. The domain object cannot protect its own invariants.
Why It Happens
Anemic models are easy to create because several common development habits point in that direction:
- Framework boundaries encourage it. ORMs, serializers, and application frameworks often distinguish entities, DTOs, repositories, and services. That separation is useful, but it does not mean every domain rule belongs in a service.
- Database-driven thinking. When you start with the database schema and generate entity classes, those classes naturally mirror table rows: columns become fields, fields get getters and setters.
- "Services are where logic goes." Teams adopt a blanket rule that business logic belongs in services. Nobody questions which logic belongs in the entity itself.
- Setter convenience. Public setters make it easy to construct and modify objects. Removing them feels restrictive until you see the bugs they enable.
- Code generation tools. Lombok's
@Dataand similar tools can generate all getters and setters automatically. That convenience can hide the question of which state changes should be legal.
The important distinction
Separating domain behavior from infrastructure is healthy. Separating every domain rule from the data it protects is the problem. A service can coordinate repositories and external APIs while the entity still owns its valid state transitions.
How to Detect It
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.