DDD Repositories vs Active Record
debt(d5/e7/b7/t5)
Closest to 'specialist tool catches it' (d5). The term's detection_hints cite phpstan and deptrac as tools that can catch violations — deptrac can enforce layer dependencies (repository interface in domain, implementation in infrastructure), and phpstan can catch type mismatches when repositories return ORM models instead of domain objects. These are specialist static analysis tools, not default linters.
Closest to 'cross-cutting refactor across the codebase' (e7). The quick_fix describes defining repository interfaces in the domain layer with domain-centric methods, which requires restructuring how persistence is handled throughout the application. Moving from Active Record (where models contain persistence) to DDD repositories (where persistence is abstracted behind interfaces) typically requires touching every place aggregates are fetched or persisted, creating in-memory implementations for testing, and restructuring namespaces.
Closest to 'strong gravitational pull' (b7). DDD repositories apply across all contexts (web, cli, queue-worker) per applies_to, meaning this architectural choice shapes how every part of the system accesses domain objects. Once you commit to repository abstractions, every new feature must go through them, every aggregate needs its repository interface, and the separation between domain and infrastructure becomes load-bearing. Not quite b9 (you can migrate incrementally), but it strongly shapes future development.
Closest to 'notable trap' (t5). The misconception field states developers believe 'DDD repositories are just a layer over the ORM' — this is a documented gotcha that most developers learning DDD eventually encounter. They expect repositories to be thin wrappers around Eloquent/Doctrine, but the pattern requires repositories to speak domain language and hide ORM details entirely. Common mistakes include returning ORM models instead of domain objects, confirming this is a learned-over-time trap rather than immediately obvious.
Also Known As
TL;DR
Explanation
The Repository pattern (DDD) provides a collection-like interface for persisting aggregates — find(), save(), delete() — with the domain model completely unaware of how it is stored. The domain object has no extends Model, no save() method, no database knowledge. Infrastructure implementations (DoctrineUserRepository, EloquentUserRepository) wire persistence behind the interface. Benefits: domain model is pure and unit-testable without a database; swapping storage is a matter of swapping the implementation. Active Record (Laravel's Eloquent model extends Model) couples domain logic and persistence in one class — convenient for simple CRUD but increasingly awkward as domain logic grows complex. Choose based on complexity: Active Record for content/admin apps, Repository for complex domain logic.
Diagram
flowchart LR
subgraph Active Record
MODEL[User extends Model] -->|knows about| DB1[(Database)]
MODEL -->|User::find| QUERY1[SQL embedded in model]
end
subgraph Repository Pattern
ENT[User entity<br/>no DB knowledge]
REPO[UserRepository<br/>interface]
IMPL[DoctrineUserRepository<br/>implements interface]
DB2[(Database)]
ENT <--> REPO
REPO -.->|implemented by| IMPL
IMPL --> DB2
end
TEST[Tests inject<br/>InMemoryUserRepository] -.->|no DB needed| REPO
style TEST fill:#238636,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Repository methods that return raw query results or ORM models instead of domain objects.
- Putting business logic in repositories — they should only handle persistence, not domain rules.
- One massive repository with 30 query methods — split by query context or use specifications.
- Not having an in-memory repository implementation for testing — the whole point of the abstraction.
Code Examples
// Repository with business logic and raw data return:
class OrderRepository {
public function getActiveOrdersOver100(): array {
return $this->db->query(
'SELECT * FROM orders WHERE status = ? AND total > 100', ['active']
)->fetchAll(); // Returns raw arrays, not domain objects — and mixes query logic
}
}
// Repository — collection abstraction over persistence
// Domain code never knows about SQL, Eloquent, or Doctrine
interface OrderRepository {
public function find(OrderId $id): ?Order;
public function findByUser(UserId $userId): OrderCollection;
public function save(Order $order): void;
public function delete(OrderId $id): void;
}
// Infrastructure implementation (hidden from domain):
class EloquentOrderRepository implements OrderRepository {
public function find(OrderId $id): ?Order {
$model = OrderModel::find($id->value());
return $model ? $this->toDomain($model) : null;
}
public function save(Order $order): void {
OrderModel::updateOrCreate(['id' => $order->getId()->value()], $this->toModel($order));
}
private function toDomain(OrderModel $m): Order { /* map to domain entity */ }
}