Anemic Domain Model (Anti-Pattern)
debt(d7/e7/b7/t7)
Closest to 'only careful code review or runtime testing' (d7). While phpstan is listed as a detection tool, the term's own metadata says automated detection is 'no'. The code pattern (domain classes with only getters/setters, logic in services) requires careful architectural code review to identify — no standard linter or SAST tool flags this anti-pattern reliably. You notice it only when the service layer becomes unmaintainably large.
Closest to 'cross-cutting refactor across the codebase' (e7). The quick_fix says 'move business logic from service classes back into the domain objects,' but this is architecturally significant: it means refactoring every service class that orchestrates domain logic, moving methods into entity classes, updating all callers, and rethinking the interaction patterns. The common_mistakes note that you often don't notice until the service layer is already unmaintainably large with duplicated logic — by that point the fix spans the entire codebase.
Closest to 'strong gravitational pull' (b7). The anemic model anti-pattern applies across all contexts (web, cli, queue-worker) and is tagged as an architectural concern. Once established, every new feature follows the 'thin model, fat service' pattern — it shapes how every developer writes code, where logic goes, and how the entire domain layer is structured. It's a persistent gravitational pull that every change must accommodate, though it stops short of b9 since the system can still function and be incrementally improved.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception is that 'separating data from logic is always clean architecture,' which feels reasonable to developers coming from procedural or service-oriented backgrounds. Many PHP frameworks (Laravel, Symfony) encourage thin models with service layers, making the anemic pattern feel like best practice. This directly contradicts OOP/DDD intent where domain objects should encapsulate behavior. Developers confuse anemic models with DTOs, and the 'thin model, fat service' approach is widely taught as good architecture.
Also Known As
TL;DR
Explanation
Martin Fowler describes the Anemic Domain Model as an anti-pattern where domain objects are data containers and all logic lives in service classes that operate on them. This is procedural programming in OO clothing: objects don't encapsulate their own behaviour, leading to scattered logic, duplication, and loss of the Tell Don't Ask principle. The alternative is a rich domain model where objects contain both data and the behaviour that operates on that data, as in DDD entities and value objects.
Diagram
flowchart LR
subgraph Anemic_Anti_Pattern
ENT2[Order entity<br/>only getters setters<br/>no behaviour]
SVC2[OrderService<br/>all business logic<br/>manipulates entities]
ENT2 <-->|data bag only| SVC2
end
subgraph Rich_Domain_Model
ORDER["Order aggregate<br/>place() ship() cancel()<br/>behaviour lives here"]
INVAR[Invariants enforced<br/>inside the object<br/>cannot be invalid]
end
subgraph Result
ANEMIC_PROB[Logic scattered<br/>duplicate validation<br/>impossible to test in isolation]
RICH_GOOD[Logic co-located with data<br/>self-validating<br/>expressive API]
end
style ENT2 fill:#f85149,color:#fff
style ANEMIC_PROB fill:#f85149,color:#fff
style ORDER fill:#238636,color:#fff
style RICH_GOOD fill:#238636,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Creating model classes with only getters and setters and putting all logic in Service or Manager classes.
- Confusing an anemic model with a DTO — DTOs are intentionally data-only; domain models should encapsulate behaviour.
- Believing that a 'thin model, fat service' architecture is always good — it inverts OOP intent for domain objects.
- Not noticing the pattern until the service layer becomes unmaintainably large and all logic is duplicated.
Avoid When
- Simple CRUD applications with no domain logic — rich domain models add complexity where there is nothing to encapsulate.
- Read-only reporting models where the data is never mutated through domain rules.
- Transaction scripts are already clear and maintainable — do not introduce domain objects for their own sake.
When To Use
- Complex domains with business rules that should live close to the data they operate on.
- When service classes are growing with repeated logic that belongs to the entity itself.
- Domain-driven design contexts where entities enforce their own invariants.
- Preventing the same business rule from being duplicated across multiple service classes.
Code Examples
// Anemic model — data bag, all logic in services
class Order {
public int $status; // magic number
public float $total;
public array $items; // zero behaviour
}
class OrderService {
public function cancel(Order $o): void {
if ($o->status !== 2) throw new \Exception('Cannot cancel');
$o->status = 5; // magic numbers everywhere
}
}
// Rich model — behaviour lives with data
class Order {
private OrderStatus $status;
public function cancel(): void {
if (!$this->status->canCancel()) throw new CannotCancelException();
$this->status = OrderStatus::Cancelled;
$this->recordEvent(new OrderCancelled($this->id));
}
public function isPaid(): bool { return $this->status === OrderStatus::Paid; }
}