The Anemic Domain Model is an anti-pattern where domain objects are just data containers with little or no behavior, and all business logic resides in separate service layers.
What It Looks Like
Anemic Model
// Just data - no behavior
public class Order
{
public int Id { get; set; }
public List<OrderLine> Lines { get; set; }
public decimal Total { get; set; }
public string Status { get; set; }
}
// All logic in services
public class OrderService
{
public void AddItem(Order order, Product product, int quantity)
{
order.Lines.Add(new OrderLine
{
ProductId = product.Id,
Quantity = quantity,
Price = product.Price
});
order.Total = order.Lines.Sum(l => l.Price * l.Quantity);
}
public void Submit(Order order)
{
if (order.Lines.Count == 0)
throw new InvalidOperationException("Cannot submit empty order");
order.Status = "Submitted";
}
}Rich Domain Model (Correct)
// Behavior with data
public class Order
{
public OrderId Id { get; private set; }
private List<OrderLine> _lines = new();
public IReadOnlyList<OrderLine> Lines => _lines;
public OrderStatus Status { get; private set; }
public void AddItem(Product product, int quantity)
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot modify submitted order");
_lines.Add(new OrderLine(product, quantity));
}
public Money CalculateTotal()
{
return _lines.Sum(l => l.Subtotal);
}
public void Submit()
{
if (!_lines.Any())
throw new InvalidOperationException("Cannot submit empty order");
Status = OrderStatus.Submitted;
DomainEvents.Raise(new OrderSubmitted(Id));
}
}Problems with Anemic Domain Model
Loses Benefits of OOP
- Breaks encapsulation
- Data and behavior separated
- No information hiding
Scattered Business Logic
- Logic spread across multiple services
- Hard to find where rules are enforced
- Duplication of validation logic
Difficult to Maintain
- Changes require updating multiple places
- Easy to bypass business rules
- No single source of truth
Poor Discoverability
- Unclear what operations are valid
- Have to read service code to understand behavior
- Domain model doesn’t express capabilities
Why It Happens
Procedural Programming Mindset
// Procedural thinking in OOP clothing
public class OrderProcessor
{
public void ProcessOrder(Order order)
{
// All logic here, order is just data
}
}Over-Separation of Concerns
Misunderstanding “separate layers” as “separate behavior from data”
Database-Centric Design
Designing domain objects to match database tables
Cargo Cult Programming
Copying patterns without understanding when to use them
How to Fix It
Move Behavior to Entities
// Before: Anemic
public class Account
{
public decimal Balance { get; set; }
}
public class AccountService
{
public void Withdraw(Account account, decimal amount)
{
if (account.Balance < amount)
throw new Exception("Insufficient funds");
account.Balance -= amount;
}
}
// After: Rich
public class Account
{
public Money Balance { get; private set; }
public void Withdraw(Money amount)
{
if (Balance < amount)
throw new InsufficientFundsException();
Balance = Balance.Subtract(amount);
DomainEvents.Raise(new MoneyWithdrawn(Id, amount));
}
}Use Value Objects
// Before: Primitives
public class Product
{
public decimal Price { get; set; }
public string Currency { get; set; }
}
// After: Value Object
public class Product
{
public Money Price { get; private set; }
public void ChangePrice(Money newPrice)
{
if (newPrice <= Money.Zero)
throw new ArgumentException("Price must be positive");
Price = newPrice;
DomainEvents.Raise(new PriceChanged(Id, newPrice));
}
}Protect Invariants
// Before: No protection
public class ShoppingCart
{
public List<CartItem> Items { get; set; } // Can be modified directly!
}
// After: Encapsulated
public class ShoppingCart
{
private readonly List<CartItem> _items = new();
public IReadOnlyList<CartItem> Items => _items;
public void AddItem(Product product, int quantity)
{
if (quantity <= 0)
throw new ArgumentException("Quantity must be positive");
var existing = _items.FirstOrDefault(i => i.ProductId == product.Id);
if (existing != null)
existing.IncreaseQuantity(quantity);
else
_items.Add(new CartItem(product, quantity));
}
}When Services Are Appropriate
Domain services are still valid for operations that don’t belong to a single entity:
// Good: Service for cross-aggregate operation
public class MoneyTransferService
{
public void Transfer(Account from, Account to, Money amount)
{
from.Withdraw(amount); // Entity method
to.Deposit(amount); // Entity method
DomainEvents.Raise(new MoneyTransferred(from.Id, to.Id, amount));
}
}Detection
Signs you have an anemic domain model:
- Entities with mostly getters/setters
- Service classes with names like
EntityNameService - Public setters everywhere
- No methods on entities (except getters/setters)
- Complex logic in application/service layer
- Validation in multiple places
Resources
Related Concepts
- Entities - Should be rich, not anemic
- Value Objects - Also should have behavior
- Domain Services - When to use services appropriately
- Domain Driven Design - Overall pattern context