- DDD
- Software Architecture
- Java
The Anemic Domain Model: the antipattern that disguises itself as DDD
The antipattern that looks like DDD but isn't, from code to refactor: when to build a rich model and when anemic is enough.

The fundamental horror of this anti-pattern is that it's so contrary to the basic idea of object-oriented design; which is to combine data and process together.
Martin Fowler
In the fundamentals post I promised to go after the classic DDD antipattern: domain logic scattered across services, with entities reduced to data bags. That antipattern has a name, the anemic domain model, and Martin Fowler was already writing about it in 2003, the same year Eric Evans' book came out.
The name comes from medicine: anemia is a deficiency of something that should be there. An anemic model looks like a domain model (it has Order, Product, Customer), but the classes only carry data. None of them have behavior. All of it lives somewhere else, usually in a class ending in Service, Manager, or Helper.
The danger is the disguise. You have a domain package, classes named after the business, maybe even Value Objects. On paper it looks like DDD. In practice it's procedural programming with objects stuck in the middle.
The snippets are in Java, but the concept holds for any object-oriented language.
What it looks like
Let's take the same e-commerce order from the previous post. The anemic version of Order is this:
public class Order {
private String id;
private String customerId;
private List<OrderItem> items = new ArrayList<>();
private String status;
private BigDecimal totalAmount;
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public List<OrderItem> getItems() { return items; }
public void setItems(List<OrderItem> items) { this.items = items; }
public BigDecimal getTotalAmount() { return totalAmount; }
public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; }
// ... the rest of the getters and setters
}Not a single business method. It's a form with get and set. And since the class does nothing, someone has to. That someone is the service:
public class OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
public void addItem(String orderId, String productId, int quantity) {
Order order = orderRepository.findById(orderId);
Product product = productRepository.findById(productId);
if (quantity <= 0) {
throw new IllegalArgumentException("Invalid quantity");
}
// check subscription eligibility
if (product.getRequiredSubscriptionLevel() > 0) {
Customer customer = customerRepository.findById(order.getCustomerId());
if (customer.getSubscriptionLevel() < product.getRequiredSubscriptionLevel()) {
throw new IllegalStateException("Insufficient subscription");
}
}
BigDecimal subtotal = product.getUnitPrice()
.multiply(BigDecimal.valueOf(quantity));
OrderItem item = new OrderItem();
item.setProductId(productId);
item.setQuantity(quantity);
item.setSubtotal(subtotal);
order.getItems().add(item);
// recompute the total
BigDecimal total = BigDecimal.ZERO;
for (OrderItem i : order.getItems()) {
total = total.add(i.getSubtotal());
}
order.setTotalAmount(total);
orderRepository.save(order);
}
public void submit(String orderId) {
Order order = orderRepository.findById(orderId);
if (order.getItems().isEmpty()) {
throw new IllegalStateException("Order has no items");
}
if (!order.getStatus().equals("CREATED")) {
throw new IllegalStateException("Order cannot be submitted");
}
order.setStatus("SUBMITTED");
orderRepository.save(order);
}
}It works. It runs in production. It passes the tests. It isn't wrong in the sense of producing a bug today, and you could argue it's just a style choice. The cost isn't correctness, it's maintenance, and it only shows up later.
Why this is a problem
The business rule has no owner
Where does the rule "quantity can't be zero or negative" live? In OrderService.addItem. And when an OrderImportService shows up that builds orders from a CSV? Or an admin endpoint that assembles an order by hand? Each one reimplements the same validation, or worse, forgets it.
The rule should sit in one place, crossed by every path. In the anemic model it floats in whichever service happened to be written first. The second service has no way of knowing it exists.
Invariants are left unprotected
Notice that Order has setStatus(String). Any piece of code in the system can do:
order.setStatus("SUBMITTED");No items. Without going through the validation. Without a valid state transition. The object accepts any string, valid or not. The Order doesn't defend itself, because it doesn't know what a valid order is. It just holds fields.
In the previous post I showed the rich Order protecting its transitions internally, with transitionTo. Here there's none of that. The object's valid state depends on everyone behaving well all the time. It's only a matter of time before they don't.
It isn't object-oriented
This is Fowler's point in the opening quote. The core idea of OO is to combine data and behavior. The anemic model splits the two: data lives in the entity, behavior lives in the service. You pay the cost of modeling with objects (classes, mappers, instances) without reaping any of the benefits (encapsulation, polymorphism, invariant protection).
What you actually have is a transaction script: a procedural sequence of steps operating over dumb data structures. That's a legitimate pattern, Fowler describes it in Patterns of Enterprise Application Architecture, and for plenty of problems it's the right call. The mistake isn't using a transaction script. The mistake is using a transaction script while thinking you're doing DDD, and carrying the complexity of both worlds.
The total lives far from the items
Look again at the loop recomputing the total in addItem. Every time an item changes, someone has to remember to run that loop again. Missed it on one path? totalAmount drifts out of sync with the items, and you get an order that claims it costs 100 but sums to 150 in line items.
In a rich model the total is derived from the items the moment you ask for it:
public Money totalAmount() {
return items.stream()
.map(OrderItem::subtotal)
.reduce(Money.ZERO, Money::plus);
}No totalAmount field to desync. Nothing to forget to update. The data and the calculation that produces it live in the same object.
How it becomes a rich model
The refactoring is about moving behavior inward. Each rule goes to the object that owns it. This is exactly the Order from the fundamentals post:
public class Order extends AggregateRoot<OrderId> {
private final OrderId id;
private final CustomerId customerId;
private final List<OrderItem> items = new ArrayList<>();
private OrderStatus status;
public void addItem(Product product, Quantity quantity,
SubscriptionCategory customerSubscription) {
product.validatePurchaseEligibility(customerSubscription);
Money subtotal = product.calculateSubtotal(quantity);
items.add(new OrderItem(product, quantity, subtotal));
}
public void submit() {
if (items.isEmpty()) {
throw new OrderValidationException("Order has no items");
}
transitionTo(OrderStatus.SUBMITTED);
addDomainEvent(new OrderSubmitted(this));
}
public Money totalAmount() {
return items.stream()
.map(OrderItem::subtotal)
.reduce(Money.ZERO, Money::plus);
}
}Notice what moved:
- The quantity validation left the service and went into
Quantity(a Value Object), which never exists with an invalid value. - The eligibility check left the service and went into
Product.validatePurchaseEligibility. The product knows who is allowed to buy it. - The status transition left
setStatus(String)and becametransitionTo, which only accepts valid transitions. There's no public status setter anymore. - The total stopped being a field and became a derived calculation.
And the service? It almost entirely disappears. It becomes the thin use case that orchestrates:
public class AddItemToOrderUseCase {
public void execute(OrderId orderId, ProductId productId, Quantity quantity) {
Order order = orderRepository.find(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
Product product = productRepository.find(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
Customer customer = customerRepository.find(order.customerId())
.orElseThrow(() -> new CustomerNotFoundException(order.customerId()));
order.addItem(product, quantity, customer.subscriptionLevel());
orderRepository.save(order);
}
}No if. No calculation. No validation. Fetch, delegate to the domain, save. That thin use case is the subject of a whole post in this series, so I won't go deeper here.
The rule of thumb is simple: behavior lives next to the data it uses. If a service method only reads getters off an object, computes something, and returns it, that method probably wanted to be a method on that object.
"But it works"
It does work. That's the annoying part. The anemic model doesn't hand you a production error on day one. It hands you a system that gets harder to change over time.
The bill arrives as the domain grows. The eligibility rule that lived in one service now lives in four. The status transition nobody protected produced a phantom order in an impossible state. The total desynced on a path nobody tested. Each of those bugs is cheap to fix in isolation, and expensive to fix when there are forty of them scattered around.
And there's the silent cost: nobody can read the business rules by looking at the domain. To understand what a valid order is, you have to hunt through every service that touches Order. The business knowledge, which was supposed to be concentrated in the model, gets diluted across the application layer.
When anemic is legitimate
Now the other side, because an anemic model isn't always a mistake.
If your domain really is a CRUD (data entry with little or no rules), a rich model is overengineering. A supplier registration screen, where "save" and "fetch" is the whole job, gains nothing by becoming an aggregate with a factory method and a domain event. There the anemic model isn't an antipattern, it's the economically correct choice.
The mistake isn't the anemic model itself. The mistake is the mismatch: using an anemic model in a domain with real, complex business rules, or building the full DDD apparatus on a domain that was just a CRUD. Both are expensive. Knowing which is which is half the job, and it's the topic of another post in this series, the one on when not to use DDD.
How to spot it in your codebase
Practical signs the model is anemic:
- Entities with only getters and setters. If the class has no method expressing a business action (
submit,cancel,addItem), it's a data bag. - Public setters for everything.
setStatus,setTotal. If anyone can put the object in any state, no invariant is protected. - Fat services full of
if. All the business logic sits there, in validations and calculations operating over other objects' getters. Manager,Helper,Utilclasses in the domain. They tend to be the dumping ground for behavior that didn't fit anywhere because the objects won't accept behavior.- Fields that have to be recomputed by hand (like
totalAmount). A sign of data that should be derived.
Found these signs and the domain has real rules? You've got a refactoring ahead. Found these signs but the domain is pure CRUD? It's probably fine, move on.
What's next
This was the most common antipattern, and the starting point for understanding the others. Several of the next posts in the series are specific cases of behavior leaking out of the domain: primitive obsession (when the Value Objects you're missing turn into loose String and BigDecimal), the domain service used as a dumping ground for logic, and the use case that does too much. All symptoms of the same disease we saw here.
Ever inherited an anemic model that called itself DDD? Or the opposite, a CRUD drowning in an aggregate it never needed? Reach out on LinkedIn, I'd love to hear about it!