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

Domain Model Pattern

quality Advanced

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 23
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
2 pings W 0 pings T 0 pings F 1 ping S 0 pings S 1 ping M 0 pings T 0 pings W 0 pings T 0 pings F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 1 ping T 0 pings F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T
No pings yet today
No pings yesterday
Amazonbot 6 Perplexity 6 Unknown AI 3 Majestic 1 ChatGPT 1 Ahrefs 1
crawler 18
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