Repositories and factories skill

Repositories and Factories are infrastructure-facing patterns in Domain-Driven Design that separate domain logic from persistence and…

by wondelai·MIT license·★ 2,235 Stars on the repo·GitHub ↗

Use now

Files of Repositories and factories

wondelai/main1 file
repositories-factories.md
Show the full text354 lines

Repositories and Factories

Repositories and Factories are infrastructure-facing patterns in Domain-Driven Design that separate domain logic from persistence and object creation concerns. The Repository provides the illusion of an in-memory collection of aggregates. The Factory encapsulates complex creation logic. Together, they keep the domain model clean and focused on business rules.

Table of Contents

  1. The Repository Pattern
  2. The Factory Pattern
  3. The Specification Pattern
  4. Ports and Adapters Relationship

The Repository Pattern

A Repository mediates between the domain and data mapping layers, acting like an in-memory collection of domain objects. Domain code uses the repository to obtain aggregates without knowing how they are stored, queried, or reconstructed.

Why Repositories Exist

Without repositories, domain logic becomes tangled with data access:

// Without repository -- domain logic polluted with SQL
def approve_claim(claim_id):
    row = db.execute("SELECT * FROM claims WHERE id = ?", claim_id)
    claim = Claim(row['id'], row['status'], row['amount'])
    claim.approve()
    db.execute("UPDATE claims SET status = ? WHERE id = ?", claim.status, claim.id)
// With repository -- domain logic is clean
def approve_claim(claim_id):
    claim = claim_repository.find_by_id(claim_id)
    claim.approve()
    claim_repository.save(claim)

The second version is readable by a domain expert. The first is not.

Repository Interface Design

The repository interface belongs in the domain layer. It speaks the ubiquitous language:

Good repository methods:

  • find_by_id(order_id) -- straightforward identity lookup
  • find_pending_orders() -- uses domain language ("pending")
  • find_by_customer(customer_id) -- domain-meaningful query
  • find_overdue_invoices(as_of_date) -- business concept in the method name

Bad repository methods:

  • get_by_status_code(3) -- magic number; what is status 3?
  • query(sql_string) -- leaks persistence technology into the domain
  • find_all_with_joins() -- technical concern, not domain language
  • get_by_column("status", "PENDING") -- generic data access, not domain query
Collection-Oriented vs. Persistence-Oriented Repositories

Eric Evans described two flavors of repository, each modeling a different metaphor:

Collection-Oriented Repository

Models the repository as an in-memory collection. You add objects to it and remove objects from it. Changes to retrieved objects are automatically tracked and persisted (like JPA/Hibernate managed entities).

interface OrderRepository:
    add(order)           # Like collection.add()
    remove(order)        # Like collection.remove()
    find_by_id(id)       # Like collection.find()
    # No explicit save() -- changes to retrieved objects are auto-tracked

Best with: ORMs that support change tracking (JPA/Hibernate, Entity Framework).

Advantages: Clean domain model; changes feel natural; no explicit save calls.

Disadvantages: "Magic" change tracking can surprise developers; harder to reason about when persistence happens.

Persistence-Oriented Repository

Models the repository as a storage mechanism. You explicitly save objects and the repository does not track changes automatically.

interface OrderRepository:
    save(order)          # Explicit persist/update
    delete(order_id)     # Explicit remove
    find_by_id(id)       # Retrieve
    # Must call save() explicitly after making changes

Best with: Frameworks without change tracking (most non-ORM approaches, event sourcing, document stores).

Advantages: Explicit control over when persistence happens; no surprises; easier to test.

Disadvantages: Must remember to call save(); risk of losing changes if save is forgotten.

Which to choose: If your persistence technology offers change tracking and your team is comfortable with it, use collection-oriented. Otherwise, use persistence-oriented. The persistence-oriented style is more common in modern applications because it is more explicit.

Repository Implementation

The repository interface lives in the domain layer. The implementation lives in the infrastructure layer. This is the Dependency Inversion Principle in action:

domain/
    model/
        Order.py              # Aggregate root
        OrderLineItem.py      # Entity within aggregate
    repository/
        OrderRepository.py    # Interface (abstract class / protocol)

infrastructure/
    persistence/
        PostgresOrderRepository.py    # Implementation
        InMemoryOrderRepository.py    # Implementation for tests

The domain layer defines what it needs (the interface). The infrastructure layer provides it (the implementation). The domain never imports from infrastructure.

What a Repository Returns

A repository always returns fully constituted aggregates -- not partial objects, not DTOs, not database rows. The aggregate returned from a repository must be in a valid state with all its invariants satisfied.

Correct: order_repository.find_by_id(id) returns an Order with all its OrderLineItems loaded, ready to have business operations performed on it.

Incorrect: order_repository.find_by_id(id) returns an OrderDTO with some fields populated and others lazily loaded. The caller must check which fields are available.

Repository Anti-Patterns
Anti-Pattern Problem Fix
Generic repository (Repository<T>) All aggregates look the same; domain-specific queries do not fit the generic interface Create specific repository interfaces per aggregate type
Repository returns DTOs DTOs are not domain objects; behavior cannot be called on them Return full aggregates; use separate read models (CQRS) for queries
Repository per entity (not per aggregate) Bypasses aggregate root; allows direct modification of internal entities One repository per aggregate root only
Repository with business logic Repository starts containing validation or transformation logic Keep repositories as pure storage; domain logic belongs in the aggregate
Repository depends on domain services Circular dependency between domain services and repositories Repositories depend only on the domain model (aggregates, value objects)

The Factory Pattern

A Factory encapsulates the logic of creating a domain object, ensuring that the object is fully formed and valid from the moment it exists. In DDD, factories are used when object creation is complex enough to warrant its own abstraction.

When to Use a Factory
Situation Factory Needed? Why
Creating a Value Object with 2-3 fields No A constructor suffices: Money(100, "USD")
Creating an aggregate with multiple parts and validation rules Yes The assembly logic is complex; a constructor would be enormous
Creating an object from an external representation (API response, file) Yes Translation from external format to domain object is a separate concern
Creating an object with conditional logic (different subtypes) Yes The decision of which subtype to create should not be in client code
Reconstituting an object from persistence Maybe If the repository handles it, a separate factory may not be needed
Factory Patterns in DDD
Factory Method on the Aggregate

The most common pattern: a static or class method on the aggregate itself:

class Order:
    @staticmethod
    def create_from_cart(cart, customer_id):
        # Validates cart is not empty
        # Converts cart items to OrderLineItems
        # Calculates initial total
        # Returns a fully valid Order
        order = Order(
            id=OrderId.generate(),
            customer_id=customer_id,
            status=OrderStatus.PENDING,
            items=[OrderLineItem.from_cart_item(item) for item in cart.items],
            total=cart.calculate_total()
        )
        order._raise_event(OrderCreated(...))
        return order

Advantages: Creation logic lives close to the aggregate. The aggregate controls its own birth.

Factory Method on Another Aggregate

When one aggregate creates another:

class Quote:
    def convert_to_order(self):
        # Quote knows how to create an Order from itself
        order = Order(
            id=OrderId.generate(),
            customer_id=self.customer_id,
            items=[OrderLineItem(item.product_id, item.quantity, item.quoted_price)
                   for item in self.line_items],
            source_quote_id=self.id
        )
        return order
Standalone Factory

When creation logic does not naturally belong to any existing aggregate:

class LoanApplicationFactory:
    def create_from_submission(self, submission, credit_report):
        # Complex assembly involving multiple inputs
        # Conditional logic based on loan type
        # Validation against business rules
        applicant = Applicant(submission.name, submission.ssn)
        risk_score = RiskScore.calculate(credit_report)

        if submission.loan_type == "mortgage":
            return MortgageApplication(applicant, risk_score, submission.property)
        elif submission.loan_type == "auto":
            return AutoLoanApplication(applicant, risk_score, submission.vehicle)
Factory Invariants

The most critical rule of factories: a factory must never produce an invalid object. If the inputs are insufficient or violate business rules, the factory must fail (throw an exception), not produce a partially valid object.

# Good -- factory enforces invariants
class Order:
    @staticmethod
    def create(customer_id, items):
        if not items:
            raise EmptyOrderError("Cannot create an order with no items")
        if not customer_id:
            raise InvalidCustomerError("Order requires a customer")
        return Order(OrderId.generate(), customer_id, items)

# Bad -- factory produces potentially invalid objects
class Order:
    @staticmethod
    def create(customer_id=None, items=None):
        return Order(OrderId.generate(), customer_id, items or [])
        # caller can now have an order with no customer and no items
Reconstitution vs. Creation

There is an important distinction between creating a new aggregate and reconstituting one from persistence:

Aspect Creation Reconstitution
When A new domain object comes into existence An existing object is loaded from storage
Validation Full business rule validation No validation needed; data was validated on creation
Domain events May raise creation events (OrderCreated) Should NOT raise events; nothing new happened
Identity Generate a new ID Use the stored ID
Invariants Enforce all invariants Assume invariants hold (data was valid when stored)

Reconstitution typically happens inside the repository implementation:

class PostgresOrderRepository:
    def find_by_id(self, order_id):
        row = self.db.query("SELECT * FROM orders WHERE id = ?", order_id)
        items = self.db.query("SELECT * FROM order_items WHERE order_id = ?", order_id)
        # Reconstitute -- no validation, no events
        return Order._reconstitute(
            id=row['id'],
            customer_id=row['customer_id'],
            status=row['status'],
            items=[OrderLineItem._reconstitute(i) for i in items]
        )

The Specification Pattern

The Specification pattern encapsulates query criteria as first-class domain objects. Instead of building queries in service code, you express criteria as composable specification objects.

Why Specifications

Without specifications, query logic scatters across the codebase:

# Query logic in a service -- not reusable, not composable
def find_risky_orders(self):
    return db.query("SELECT * FROM orders WHERE total > 10000 AND customer_risk > 7")

# Same logic duplicated elsewhere with slight variations
def find_very_risky_orders(self):
    return db.query("SELECT * FROM orders WHERE total > 50000 AND customer_risk > 9")

With specifications:

high_value = OrderValueExceeds(10000)
high_risk = CustomerRiskAbove(7)
risky_orders = order_repository.find_matching(high_value.and_(high_risk))

very_risky = OrderValueExceeds(50000).and_(CustomerRiskAbove(9))
very_risky_orders = order_repository.find_matching(very_risky)
Specification Composition

Specifications compose using logical operators:

Operator Meaning Example
and_ Both must be true HighValue.and_(HighRisk)
or_ Either must be true HighValue.or_(HighRisk)
not_ Must not be true not_(Cancelled)
Specifications in the Domain Layer

The specification interface lives in the domain layer. Implementations can be in the domain (for in-memory filtering) or infrastructure (for database queries):

# Domain layer -- specification interface
class Specification:
    def is_satisfied_by(self, candidate) -> bool:
        pass

class OverdueInvoice(Specification):
    def __init__(self, as_of_date):
        self.as_of_date = as_of_date

    def is_satisfied_by(self, invoice):
        return invoice.due_date < self.as_of_date and not invoice.is_paid

Ports and Adapters Relationship

Repositories and Factories fit naturally into the Ports and Adapters (Hexagonal) architecture:

                    Domain Layer
                   ┌─────────────────────────┐
                   │  Aggregates             │
                   │  Value Objects           │
                   │  Domain Events           │
                   │  Repository Interfaces ──┼── Port (interface)
                   │  Factory Interfaces   ──┼── Port (interface)
                   └─────────────────────────┘
                              │
                              │ implements
                              ▼
                   Infrastructure Layer
                   ┌─────────────────────────┐
                   │  PostgresOrderRepo    ──┼── Adapter (implementation)
                   │  InMemoryOrderRepo    ──┼── Adapter (for tests)
                   │  S3DocumentFactory    ──┼── Adapter (implementation)
                   └─────────────────────────┘

The key principle: The domain defines what it needs (ports). Infrastructure provides it (adapters). Dependencies point inward -- infrastructure depends on domain, never the reverse.

This means:

  • The domain layer has zero imports from infrastructure packages
  • Repository interfaces use domain types (Order, OrderId), not infrastructure types (Row, Document)
  • The application can swap persistence technologies by providing a new adapter without touching domain code
  • Tests use in-memory adapters to test domain logic without databases
1# Repositories and Factories
2 
3Repositories and Factories are infrastructure-facing patterns in Domain-Driven Design that separate domain logic from persistence and object creation concerns. The Repository provides the illusion of an in-memory collection of aggregates. The Factory encapsulates complex creation logic. Together, they keep the domain model clean and focused on business rules.
4 
5 
6## Table of Contents
71. [The Repository Pattern](#the-repository-pattern)
82. [The Factory Pattern](#the-factory-pattern)
93. [The Specification Pattern](#the-specification-pattern)
104. [Ports and Adapters Relationship](#ports-and-adapters-relationship)
11 
12---
13 
14## The Repository Pattern
15 
16A Repository mediates between the domain and data mapping layers, acting like an in-memory collection of domain objects. Domain code uses the repository to obtain aggregates without knowing how they are stored, queried, or reconstructed.
17 
18### Why Repositories Exist
19 
20Without repositories, domain logic becomes tangled with data access:
21 
22```
23// Without repository -- domain logic polluted with SQL
24def approve_claim(claim_id):
25 row = db.execute("SELECT * FROM claims WHERE id = ?", claim_id)
26 claim = Claim(row['id'], row['status'], row['amount'])
27 claim.approve()
28 db.execute("UPDATE claims SET status = ? WHERE id = ?", claim.status, claim.id)
29```
30 
31```
32// With repository -- domain logic is clean
33def approve_claim(claim_id):
34 claim = claim_repository.find_by_id(claim_id)
35 claim.approve()
36 claim_repository.save(claim)
37```
38 
39The second version is readable by a domain expert. The first is not.
40 
41### Repository Interface Design
42 
43The repository interface belongs in the domain layer. It speaks the ubiquitous language:
44 
45**Good repository methods:**
46- `find_by_id(order_id)` -- straightforward identity lookup
47- `find_pending_orders()` -- uses domain language ("pending")
48- `find_by_customer(customer_id)` -- domain-meaningful query
49- `find_overdue_invoices(as_of_date)` -- business concept in the method name
50 
51**Bad repository methods:**
52- `get_by_status_code(3)` -- magic number; what is status 3?
53- `query(sql_string)` -- leaks persistence technology into the domain
54- `find_all_with_joins()` -- technical concern, not domain language
55- `get_by_column("status", "PENDING")` -- generic data access, not domain query
56 
57### Collection-Oriented vs. Persistence-Oriented Repositories
58 
59Eric Evans described two flavors of repository, each modeling a different metaphor:
60 
61#### Collection-Oriented Repository
62 
63Models the repository as an in-memory collection. You add objects to it and remove objects from it. Changes to retrieved objects are automatically tracked and persisted (like JPA/Hibernate managed entities).
64 
65```
66interface OrderRepository:
67 add(order) # Like collection.add()
68 remove(order) # Like collection.remove()
69 find_by_id(id) # Like collection.find()
70 # No explicit save() -- changes to retrieved objects are auto-tracked
71```
72 
73**Best with:** ORMs that support change tracking (JPA/Hibernate, Entity Framework).
74 
75**Advantages:** Clean domain model; changes feel natural; no explicit save calls.
76 
77**Disadvantages:** "Magic" change tracking can surprise developers; harder to reason about when persistence happens.
78 
79#### Persistence-Oriented Repository
80 
81Models the repository as a storage mechanism. You explicitly save objects and the repository does not track changes automatically.
82 
83```
84interface OrderRepository:
85 save(order) # Explicit persist/update
86 delete(order_id) # Explicit remove
87 find_by_id(id) # Retrieve
88 # Must call save() explicitly after making changes
89```
90 
91**Best with:** Frameworks without change tracking (most non-ORM approaches, event sourcing, document stores).
92 
93**Advantages:** Explicit control over when persistence happens; no surprises; easier to test.
94 
95**Disadvantages:** Must remember to call save(); risk of losing changes if save is forgotten.
96 
97**Which to choose:** If your persistence technology offers change tracking and your team is comfortable with it, use collection-oriented. Otherwise, use persistence-oriented. The persistence-oriented style is more common in modern applications because it is more explicit.
98 
99### Repository Implementation
100 
101The repository interface lives in the domain layer. The implementation lives in the infrastructure layer. This is the Dependency Inversion Principle in action:
102 
103```
104domain/
105 model/
106 Order.py # Aggregate root
107 OrderLineItem.py # Entity within aggregate
108 repository/
109 OrderRepository.py # Interface (abstract class / protocol)
110 
111infrastructure/
112 persistence/
113 PostgresOrderRepository.py # Implementation
114 InMemoryOrderRepository.py # Implementation for tests
115```
116 
117The domain layer defines what it needs (the interface). The infrastructure layer provides it (the implementation). The domain never imports from infrastructure.
118 
119### What a Repository Returns
120 
121A repository always returns fully constituted aggregates -- not partial objects, not DTOs, not database rows. The aggregate returned from a repository must be in a valid state with all its invariants satisfied.
122 
123**Correct:** `order_repository.find_by_id(id)` returns an `Order` with all its `OrderLineItems` loaded, ready to have business operations performed on it.
124 
125**Incorrect:** `order_repository.find_by_id(id)` returns an `OrderDTO` with some fields populated and others lazily loaded. The caller must check which fields are available.
126 
127### Repository Anti-Patterns
128 
129| Anti-Pattern | Problem | Fix |
130|-------------|---------|-----|
131| Generic repository (`Repository<T>`) | All aggregates look the same; domain-specific queries do not fit the generic interface | Create specific repository interfaces per aggregate type |
132| Repository returns DTOs | DTOs are not domain objects; behavior cannot be called on them | Return full aggregates; use separate read models (CQRS) for queries |
133| Repository per entity (not per aggregate) | Bypasses aggregate root; allows direct modification of internal entities | One repository per aggregate root only |
134| Repository with business logic | Repository starts containing validation or transformation logic | Keep repositories as pure storage; domain logic belongs in the aggregate |
135| Repository depends on domain services | Circular dependency between domain services and repositories | Repositories depend only on the domain model (aggregates, value objects) |
136 
137## The Factory Pattern
138 
139A Factory encapsulates the logic of creating a domain object, ensuring that the object is fully formed and valid from the moment it exists. In DDD, factories are used when object creation is complex enough to warrant its own abstraction.
140 
141### When to Use a Factory
142 
143| Situation | Factory Needed? | Why |
144|-----------|----------------|-----|
145| Creating a Value Object with 2-3 fields | No | A constructor suffices: `Money(100, "USD")` |
146| Creating an aggregate with multiple parts and validation rules | Yes | The assembly logic is complex; a constructor would be enormous |
147| Creating an object from an external representation (API response, file) | Yes | Translation from external format to domain object is a separate concern |
148| Creating an object with conditional logic (different subtypes) | Yes | The decision of which subtype to create should not be in client code |
149| Reconstituting an object from persistence | Maybe | If the repository handles it, a separate factory may not be needed |
150 
151### Factory Patterns in DDD
152 
153#### Factory Method on the Aggregate
154 
155The most common pattern: a static or class method on the aggregate itself:
156 
157```
158class Order:
159 @staticmethod
160 def create_from_cart(cart, customer_id):
161 # Validates cart is not empty
162 # Converts cart items to OrderLineItems
163 # Calculates initial total
164 # Returns a fully valid Order
165 order = Order(
166 id=OrderId.generate(),
167 customer_id=customer_id,
168 status=OrderStatus.PENDING,
169 items=[OrderLineItem.from_cart_item(item) for item in cart.items],
170 total=cart.calculate_total()
171 )
172 order._raise_event(OrderCreated(...))
173 return order
174```
175 
176**Advantages:** Creation logic lives close to the aggregate. The aggregate controls its own birth.
177 
178#### Factory Method on Another Aggregate
179 
180When one aggregate creates another:
181 
182```
183class Quote:
184 def convert_to_order(self):
185 # Quote knows how to create an Order from itself
186 order = Order(
187 id=OrderId.generate(),
188 customer_id=self.customer_id,
189 items=[OrderLineItem(item.product_id, item.quantity, item.quoted_price)
190 for item in self.line_items],
191 source_quote_id=self.id
192 )
193 return order
194```
195 
196#### Standalone Factory
197 
198When creation logic does not naturally belong to any existing aggregate:
199 
200```
201class LoanApplicationFactory:
202 def create_from_submission(self, submission, credit_report):
203 # Complex assembly involving multiple inputs
204 # Conditional logic based on loan type
205 # Validation against business rules
206 applicant = Applicant(submission.name, submission.ssn)
207 risk_score = RiskScore.calculate(credit_report)
208 
209 if submission.loan_type == "mortgage":
210 return MortgageApplication(applicant, risk_score, submission.property)
211 elif submission.loan_type == "auto":
212 return AutoLoanApplication(applicant, risk_score, submission.vehicle)
213```
214 
215### Factory Invariants
216 
217The most critical rule of factories: **a factory must never produce an invalid object.** If the inputs are insufficient or violate business rules, the factory must fail (throw an exception), not produce a partially valid object.
218 
219```
220# Good -- factory enforces invariants
221class Order:
222 @staticmethod
223 def create(customer_id, items):
224 if not items:
225 raise EmptyOrderError("Cannot create an order with no items")
226 if not customer_id:
227 raise InvalidCustomerError("Order requires a customer")
228 return Order(OrderId.generate(), customer_id, items)
229 
230# Bad -- factory produces potentially invalid objects
231class Order:
232 @staticmethod
233 def create(customer_id=None, items=None):
234 return Order(OrderId.generate(), customer_id, items or [])
235 # caller can now have an order with no customer and no items
236```
237 
238### Reconstitution vs. Creation
239 
240There is an important distinction between creating a new aggregate and reconstituting one from persistence:
241 
242| Aspect | Creation | Reconstitution |
243|--------|---------|----------------|
244| When | A new domain object comes into existence | An existing object is loaded from storage |
245| Validation | Full business rule validation | No validation needed; data was validated on creation |
246| Domain events | May raise creation events (`OrderCreated`) | Should NOT raise events; nothing new happened |
247| Identity | Generate a new ID | Use the stored ID |
248| Invariants | Enforce all invariants | Assume invariants hold (data was valid when stored) |
249 
250Reconstitution typically happens inside the repository implementation:
251 
252```
253class PostgresOrderRepository:
254 def find_by_id(self, order_id):
255 row = self.db.query("SELECT * FROM orders WHERE id = ?", order_id)
256 items = self.db.query("SELECT * FROM order_items WHERE order_id = ?", order_id)
257 # Reconstitute -- no validation, no events
258 return Order._reconstitute(
259 id=row['id'],
260 customer_id=row['customer_id'],
261 status=row['status'],
262 items=[OrderLineItem._reconstitute(i) for i in items]
263 )
264```
265 
266## The Specification Pattern
267 
268The Specification pattern encapsulates query criteria as first-class domain objects. Instead of building queries in service code, you express criteria as composable specification objects.
269 
270### Why Specifications
271 
272Without specifications, query logic scatters across the codebase:
273 
274```
275# Query logic in a service -- not reusable, not composable
276def find_risky_orders(self):
277 return db.query("SELECT * FROM orders WHERE total > 10000 AND customer_risk > 7")
278 
279# Same logic duplicated elsewhere with slight variations
280def find_very_risky_orders(self):
281 return db.query("SELECT * FROM orders WHERE total > 50000 AND customer_risk > 9")
282```
283 
284With specifications:
285 
286```
287high_value = OrderValueExceeds(10000)
288high_risk = CustomerRiskAbove(7)
289risky_orders = order_repository.find_matching(high_value.and_(high_risk))
290 
291very_risky = OrderValueExceeds(50000).and_(CustomerRiskAbove(9))
292very_risky_orders = order_repository.find_matching(very_risky)
293```
294 
295### Specification Composition
296 
297Specifications compose using logical operators:
298 
299| Operator | Meaning | Example |
300|----------|---------|---------|
301| `and_` | Both must be true | `HighValue.and_(HighRisk)` |
302| `or_` | Either must be true | `HighValue.or_(HighRisk)` |
303| `not_` | Must not be true | `not_(Cancelled)` |
304 
305### Specifications in the Domain Layer
306 
307The specification interface lives in the domain layer. Implementations can be in the domain (for in-memory filtering) or infrastructure (for database queries):
308 
309```
310# Domain layer -- specification interface
311class Specification:
312 def is_satisfied_by(self, candidate) -> bool:
313 pass
314 
315class OverdueInvoice(Specification):
316 def __init__(self, as_of_date):
317 self.as_of_date = as_of_date
318 
319 def is_satisfied_by(self, invoice):
320 return invoice.due_date < self.as_of_date and not invoice.is_paid
321```
322 
323## Ports and Adapters Relationship
324 
325Repositories and Factories fit naturally into the Ports and Adapters (Hexagonal) architecture:
326 
327```
328 Domain Layer
329 ┌─────────────────────────┐
330 │ Aggregates │
331 │ Value Objects │
332 │ Domain Events │
333 │ Repository Interfaces ──┼── Port (interface)
334 │ Factory Interfaces ──┼── Port (interface)
335 └─────────────────────────┘
336 │
337 │ implements
338 ▼
339 Infrastructure Layer
340 ┌─────────────────────────┐
341 │ PostgresOrderRepo ──┼── Adapter (implementation)
342 │ InMemoryOrderRepo ──┼── Adapter (for tests)
343 │ S3DocumentFactory ──┼── Adapter (implementation)
344 └─────────────────────────┘
345```
346 
347**The key principle:** The domain defines what it needs (ports). Infrastructure provides it (adapters). Dependencies point inward -- infrastructure depends on domain, never the reverse.
348 
349This means:
350- The domain layer has zero imports from infrastructure packages
351- Repository interfaces use domain types (`Order`, `OrderId`), not infrastructure types (`Row`, `Document`)
352- The application can swap persistence technologies by providing a new adapter without touching domain code
353- Tests use in-memory adapters to test domain logic without databases
354 

Discussion

Alternatives