← CodeClarityLab Home
Browse by Category
+ added · updated 7d
← Back to glossary

Anemic Domain Model (Anti-Pattern)

quality Advanced

Also Known As

anemic model data class behaviour-free model

TL;DR

Domain objects with only data (getters/setters) and no behaviour — business logic scattered across service classes.

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

Separating data (models) from logic (services) is always clean architecture. When domain logic lives entirely in service classes and models are just property bags, you lose encapsulation, invariant enforcement, and the benefits of object-oriented design.

Why It Matters

Anemic domain models push business logic into service classes, scattering it across the codebase — the domain model becomes a passive data bag that requires external orchestration to do anything meaningful.

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

✗ Vulnerable
// 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
    }
}
✓ Fixed
// 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; }
}

Added 15 Mar 2026
Edited 19 Apr 2026
Views 38
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 1 ping S 0 pings S 0 pings M 0 pings T 1 ping W 0 pings T 2 pings F 0 pings S 0 pings S 1 ping M 1 ping T 6 pings W 0 pings T
No pings yet today
Perplexity 2 ChatGPT 2 Amazonbot 1 Unknown AI 1
Perplexity 8 ChatGPT 8 Amazonbot 6 Unknown AI 3 Google 2 Ahrefs 2 SEMrush 2
crawler 29 crawler_json 2
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: High
⚡ Quick Fix
Move business logic from service classes back into the domain objects that own the data — if your Order class only has getters/setters, it's anemic
📦 Applies To
any web cli queue-worker
🔗 Prerequisites
🔍 Detection Hints
Domain classes with only getters/setters; all business logic in service or manager classes; OrderService.cancelOrder() not Order.cancel()
Auto-detectable: ✗ No phpstan
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: High ✗ Manual fix Fix: High Context: Class Tests: Update

✓ schema.org compliant