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

Domain Model 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). The detection_hints list phpstan and deptrac as tools, but the metadata explicitly states automated=no. The code_pattern (service classes with 20+ methods, entities with only getters/setters) requires human review to identify — phpstan can flag some structural issues and deptrac can catch dependency violations, but neither reliably detects an anemic domain model or misplaced business logic as a pattern. A competent reviewer must recognize the antipattern across the codebase.

e7 Effort Remediation debt — work required to fix once spotted

Closest to 'cross-cutting refactor across the codebase' (e7). The quick_fix describes moving business rules into domain classes, but common_mistakes reveal multiple systemic problems: ORM coupling, pervasive getters/setters, primitive obsession, and logic crossing aggregate boundaries. This is not a single-component fix — it requires restructuring entities, removing ORM base class inheritance, introducing value objects, and replacing service logic throughout the codebase. This is a cross-cutting refactor that touches many files.

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

Closest to 'strong gravitational pull' (e7). The applies_to field covers web, cli, and queue-worker contexts — the full breadth of the application. A poorly implemented domain model (or its absence — an anemic model) shapes every feature addition: teams must decide where to put logic for every new business rule, services accumulate methods, and the structural debt grows with each change. The choice defines how business logic is organized system-wide, making it a persistent productivity tax with strong gravitational pull.

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

Closest to 'serious trap' (t7). The misconception field explicitly states that developers conflate domain models with database models, treating them as the same class. This leads to ORM base class coupling (first common mistake) and getters/setters on all fields (second common mistake), which is the exact opposite of a rich domain model. Developers from ActiveRecord or Django ORM backgrounds will strongly expect domain and persistence to be the same object — a directly contradicted assumption from other frameworks/patterns they already know.

About DEBT scoring →

Also Known As

rich domain model domain object business object

TL;DR

An object model of the domain that incorporates both behaviour and data — entities with methods expressing domain operations rather than just data containers.

Explanation

The Domain Model pattern (Martin Fowler, PoEAA) puts business logic inside the domain objects themselves. Unlike Anemic Domain Model (data containers with no behaviour) or Transaction Script (logic in service layer), a rich domain model has Order::place(), Invoice::generate(), and User::grantPermission() — the objects do things. This aligns with DDD aggregates and is the foundation for DDD tactical patterns. Best for: complex business logic with many rules and invariants. Overkill for: simple CRUD with few rules.

Common Misconception

Domain models and database models are the same thing — domain models represent business concepts and rules; database models represent storage concerns; they should be separate classes.

Why It Matters

A domain model that enforces business invariants in its methods makes invalid states unrepresentable — you cannot have an Order in an impossible state if the Order class prevents it in its own methods.

Common Mistakes

  • Domain model extending ORM base class — couples domain to persistence, prevents pure unit testing.
  • Getters and setters on all fields — allows external code to put the object in invalid state.
  • Domain methods that take primitive arguments instead of value objects — Order::apply(42) vs Order::apply(new Discount(42, 'percent')).
  • Logic that crosses aggregate boundaries in a single method — use domain events instead.

Code Examples

✗ Vulnerable
// Anemic model — no behaviour:
class Order {
    public int $status;
    public float $total;
    // No methods — just data
}

// Logic in service — scattered:
class OrderService {
    public function ship(Order $order): void {
        $order->status = 3; // Magic number
        $order->shipped_at = now(); // Direct mutation
    }
}
✓ Fixed
// Rich domain model — behaviour in the object:
class Order {
    private OrderStatus $status;
    private Money $total;

    public function ship(TrackingNumber $tracking): void {
        if (!$this->status->canBeShipped()) {
            throw new InvalidOrderTransition('Order cannot be shipped from ' . $this->status);
        }
        $this->status = OrderStatus::Shipped;
        $this->trackingNumber = $tracking;
        $this->raise(new OrderShipped($this->id, $tracking));
    }
}

Added 16 Mar 2026
Edited 22 Mar 2026
Views 49
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 1 ping 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 1 ping S 4 pings S 0 pings M 0 pings T 0 pings W 3 pings T 1 ping F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 1 ping M 0 pings T 0 pings W
No pings yet today
No pings yesterday
Amazonbot 7 Perplexity 6 Scrapy 6 ChatGPT 3 Unknown AI 3 Ahrefs 3 Claude 2 SEMrush 2 Majestic 1 Bing 1 Meta AI 1 Google 1 Sogou 1 PetalBot 1
crawler 36 crawler_json 2
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: High
⚡ Quick Fix
Move business rules into the domain classes they belong to — if an Order knows its own rules (can it be cancelled? is it complete?), services become thin coordinators not rule engines
📦 Applies To
any web cli queue-worker
🔗 Prerequisites
🔍 Detection Hints
Service class with 20+ methods implementing all business rules; domain entities with only getters/setters; methods like canCancel() in OrderService not Order
Auto-detectable: ✗ No phpstan deptrac
⚠ Related Problems
🤖 AI Agent
Confidence: Low False Positives: High ✗ Manual fix Fix: High Context: File Tests: Update


✓ schema.org compliant