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

DDD Repositories vs Active Record

Architecture Advanced
debt(d5/e7/b7/t5)
d5 Detectability Operational debt — how invisible misuse is to your safety net

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.

e7 Effort Remediation debt — work required to fix once spotted

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.

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

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.

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

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.

About DEBT scoring →

Also Known As

DDD repository aggregate repository domain repository

TL;DR

Repository pattern separates persistence from domain logic — the opposite of Active Record where the model knows how to save itself.

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

DDD repositories are just a layer over the ORM. A DDD repository provides a collection-like interface for aggregates — it speaks the domain language (findByCustomerId) not the database language. The ORM is an infrastructure detail hidden behind the repository interface.

Why It Matters

DDD repositories provide a collection-like abstraction for accessing aggregates — they hide persistence details from the domain, allowing the domain to be tested without a real database.

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

✗ Vulnerable
// 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
    }
}
✓ Fixed
// 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 */ }
}

Added 15 Mar 2026
Edited 22 Mar 2026
Views 120
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
1 ping W 0 pings T 2 pings F 0 pings S 2 pings S 1 ping M 0 pings T 0 pings W 1 ping T 0 pings F 0 pings S 0 pings S 0 pings M 1 ping T 0 pings W 1 ping T 0 pings F 0 pings S 1 ping S 0 pings M 0 pings T 1 ping W 0 pings T 0 pings F 0 pings S 0 pings S 1 ping M 0 pings T 2 pings W 0 pings T
No pings yet today
Bing 2
PetalBot 13 Amazonbot 10 Ahrefs 8 SEMrush 7 Scrapy 6 Perplexity 5 Bing 5 Google 3 Majestic 3 ChatGPT 2 Twitter/X 2 Applebot 2 Unknown AI 2 Meta AI 1 Qwen 1
crawler 67 crawler_json 3
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: High
⚡ Quick Fix
Define the repository interface in your domain layer with domain-centric methods (findByEmail, findActiveOrders) — the infrastructure layer implements it; the domain never knows about Doctrine or Eloquent
📦 Applies To
any web cli queue-worker
🔗 Prerequisites
🔍 Detection Hints
Repository extending Doctrine EntityRepository or Eloquent Model; repository interface defined in infrastructure namespace not domain
Auto-detectable: ✓ Yes phpstan deptrac
⚠ Related Problems
🤖 AI Agent
Confidence: Low False Positives: High ✗ Manual fix Fix: High Context: Class Tests: Update


✓ schema.org compliant