Domain Model Pattern
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));
}
}
Tags
🤝 Adopt this term
£79/year · your link shown here
Added
16 Mar 2026
Edited
22 Mar 2026
Views
23
🤖 AI Guestbook educational data only
|
|
Last 30 days
Agents 0
No pings yet today
No pings yesterday
Amazonbot 6
Perplexity 6
Unknown AI 3
Majestic 1
ChatGPT 1
Ahrefs 1
Also referenced
How they use it
crawler 18
Related categories
⚡
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