Jordi Salazar
ExperienceArticles
Jordi Salazar
HomeExperienceArticles
HomeExperienceArticles
The Anemic Domain: the silent antipattern Vaughn Vernon warns us to avoid

The Anemic Domain: the silent antipattern Vaughn Vernon warns us to avoid

Why a domain model without behavior betrays the essence of DDD, and how to build rich entities following the principles of Implementing Domain-Driven Design.

March 9, 2026
14 min read
DDDDomain-Driven DesignSoftware ArchitectureVaughn Vernon

Enjoyed this article? Share it.

In far too many enterprise projects, domain classes are little more than data structures with getters and setters. All the business logic lives in "services" that manipulate those structures from the outside. Vaughn Vernon, in his book Implementing Domain-Driven Design (2013), devotes considerable effort to explaining why this is an antipattern — and how to build the correct alternative.

What is the Anemic Domain Model

The term was originally coined by Martin Fowler, but Vernon takes it up forcefully in the context of DDD. An anemic domain model is one where:

  • Entities contain only state (attributes and their accessors).
  • All behavior (business rules, validations, state transitions) resides in application services or, worse still, in controllers.
  • The model "looks" object-oriented, but in reality it is procedural programming in disguise.
// Anemic Model — the entity is just a bag of data
public class Order {
    private String id;
    private String status;
    private BigDecimal total;
    private List<OrderLine> lines;
 
    // Only getters and setters...
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
    public void setTotal(BigDecimal total) { this.total = total; }
}
// The service does ALL the work
public class OrderService {
    public void confirmOrder(Order order) {
        if (!order.getStatus().equals("PENDING")) {
            throw new IllegalStateException("Cannot confirm");
        }
        BigDecimal total = BigDecimal.ZERO;
        for (OrderLine line : order.getLines()) {
            total = total.add(line.getPrice().multiply(
                BigDecimal.valueOf(line.getQuantity())));
        }
        order.setTotal(total);
        order.

The problem seems subtle, but it runs deep: the model does not protect its own invariants. Any code with access to the setter can put the entity in an invalid state.

Why Vernon considers it an antipattern

Vernon argues that the anemic domain violates the fundamental principle of DDD: that the domain model should be the most faithful possible representation of business knowledge. When rules are scattered across services, you lose:

  1. Ubiquitous Language — The code no longer speaks the language of the domain. order.confirm() is far more expressive than orderService.confirmOrder(order). When a business expert says "the order is confirmed," they are describing an action of the order, not of an external service.

  2. Encapsulation of invariants — Business rules are exposed and duplicated. If two different services need to confirm an order, both reimplement (or copy) the validation. It is only a matter of time before they diverge.

  3. Model expressiveness — A rich model communicates intent. An anemic model requires you to read the service to understand what an entity can do.

"If the domain objects have no behavior, then you don't have a domain model — you have a data model." — Vaughn Vernon, Implementing Domain-Driven Design

The alternative: a rich domain model

Vernon proposes that entities and aggregates should encapsulate their own behavior. The entity is the guardian of its invariants:

// Rich Model — the entity protects its invariants
public class Order {
    private OrderId id;
    private OrderStatus status;
    private Money total;
    private List<OrderLine> lines;
 
    public Order(OrderId id, List<OrderLine> lines) {
        if (lines == null || lines.isEmpty()) {
            throw new IllegalArgumentException(
                "An order must have at least one line");
        }
        this.id = id;
        this.lines = List.copyOf(lines);
        this.status 


























The key differences:

  • No public setters for critical state. State only changes through methods with domain-meaningful names (confirm(), cancel()).
  • Invariants are validated in the constructor — it is impossible to create an order without lines.
  • The total is calculated internally — no external code can set an incorrect total.
  • Methods use the ubiquitous language — order.confirm() is exactly what a business expert would say.

The role of the application service

Vernon does not say that services disappear. Application services still exist, but their responsibility changes radically:

public class OrderApplicationService {
    private final OrderRepository repository;
    private final EventPublisher events;
 
    public void confirmOrder(ConfirmOrderCommand cmd) {
        Order order = repository.findById(cmd.orderId());
 
        order.confirm();  // The logic lives in the domain
 
        repository.save(order);
        events.publish(new OrderConfirmed(order.id()));
    }
}

The application service orchestrates, it does not decide. Its job is to:

  • Load the aggregate from the repository.
  • Invoke the domain method.
  • Persist the changes.
  • Publish events if needed.

All the business logic is in order.confirm(), not in the service.

Signs that your model is anemic

Vernon describes several symptoms that betray an anemic model in a real project:

  • Services with hundreds of lines that manipulate entities field by field.
  • Entities that are essentially DTOs — they have getters and setters, but no method with business meaning.
  • Duplicated business rules across multiple services that operate on the same entity.
  • Unit tests that only test services, never entities — because the entities have nothing to test.
  • Business changes that require modifying multiple services instead of a single aggregate.

Why we fall into the anemic model

It is tempting to wonder: if it is such an obvious antipattern, why is it so common? There are several practical reasons:

  1. Frameworks that encourage it — many ORM frameworks require empty constructors and public setters for hydration. This pushes the developer to expose all state.

  2. Misunderstood separation — "separating logic into services" sounds like good architecture, but it confuses separation of concerns with extracting behavior from the place where it belongs.

  3. Data model inertia — teams that design the database first tend to create entities that mirror tables, not domain concepts.

  4. Short-term convenience — a setter is quicker to write than thinking about the correct domain method. The debt is paid later.

How to migrate: the pragmatic approach

If you already have an anemic model in production, Vernon suggests an incremental approach:

  1. Identify the most critical aggregate — the one where business rules are most complex and the risk of inconsistency is greatest.

  2. Move one behavior at a time — take a method from the service and turn it into a method on the entity. Start with validations and state transitions.

  3. Eliminate the setters — replace them with intention-revealing methods. setStatus("CONFIRMED") becomes confirm().

  4. Protect the constructor — ensure that an instance cannot be created in an invalid state.

  5. Adjust the tests — unit tests should now cover the entity directly. If order.confirm() throws an exception when the state is not PENDING, that is a domain test.

You do not need to rewrite the entire system at once. Every setter you remove and every domain method you create is a step toward a more expressive and robust model.

Connection with other DDD concepts

The rich model does not exist in isolation. Vernon connects it with other DDD building blocks:

  • Value Objects — immutable types like Money, OrderId, or EmailAddress that replace primitives and encapsulate validation. In the example above, Money knows how to add amounts and OrderId guarantees a valid format.

  • Aggregates — Order with its OrderLine items form an aggregate. The aggregate root (Order) is the only entry point for modifying the set. No one accesses an OrderLine directly to change its price.

  • Domain Events — when order.confirm() executes, it can register an OrderConfirmed event that other bounded contexts will consume. The event originates in the domain, not in the service.

Conclusion

The anemic domain model is comfortable at the start and costly in the long run. Vernon reminds us that DDD is not just tactical or strategic patterns — it is a way of thinking about software where the domain model is the heart of the system, not a mere data container.

If your entities only have getters and setters, you are not doing DDD. You are doing procedural programming with objects.

The next time you are about to write entity.setStatus(newStatus), ask yourself: what business operation does this change represent? And give that operation a name that a domain expert would recognize.

setStatus
(
"CONFIRMED"
);
}
}
=
OrderStatus.PENDING;
this.total = calculateTotal();
}
public void confirm() {
if (this.status != OrderStatus.PENDING) {
throw new IllegalStateException(
"Only a pending order can be confirmed");
}
this.status = OrderStatus.CONFIRMED;
}
public void cancel(String reason) {
if (this.status == OrderStatus.SHIPPED) {
throw new IllegalStateException(
"A shipped order cannot be cancelled");
}
this.status = OrderStatus.CANCELLED;
}
private Money calculateTotal() {
return lines.stream()
.map(OrderLine::subtotal)
.reduce(Money.ZERO, Money::add);
}
// No setStatus(), no setTotal()
}