Domain Model Pattern
debt(d7/e7/b7/t7)
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.
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.
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.
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.
Also Known As
TL;DR
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
Why It Matters
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
// 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
}
}
// 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));
}
}