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

Anemic Domain Model (Anti-Pattern)

Code Quality Advanced
debt(d7/e7/b7/t7)
d7 Detectability Operational debt — how invisible misuse is to your safety net

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.

e7 Effort Remediation debt — work required to fix once spotted

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.

b7 Burden Structural debt — long-term weight of choosing wrong

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.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

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.

About DEBT scoring →

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 75
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings W 1 ping 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 4 pings S 3 pings S 3 pings M 1 ping T 0 pings W 0 pings T 0 pings F 1 ping S 0 pings S 0 pings M 4 pings T 0 pings W 0 pings T 1 ping F 0 pings S 0 pings S 3 pings M 1 ping T 0 pings W
No pings yet today
SEMrush 1
Perplexity 11 Scrapy 11 ChatGPT 8 Amazonbot 7 Google 5 Ahrefs 4 Unknown AI 3 SEMrush 3 Claude 2 Bing 2 Sogou 2 Meta AI 1 PetalBot 1
crawler 55 crawler_json 5
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