
Created on: 9/12/2025
Updated on: 9/5/2026
4 mins
Tactical Design with Repositories
In our previous articles we looked at aggregates that protect business rules and domain events that communicate state changes.
Together, they form the core behavior of a domain model.
But in real-world systems, aggregates cannot simply live in memory forever.
They must be stored, retrieved, and updated without leaking database plumbing into domain logic.
This is where the Repository pattern and Ports and Adapters come into play.
They give us a clean way to manage aggregate persistence while keeping the domain model completely independent of infrastructure.
What Is a Repository?
In Domain-Driven Design, a repository represents an in-memory collection of aggregates.
From the perspective of the domain:
- You ask it to find an aggregate by its unique identity.
- You add a newly created aggregate to it.
- You update an existing aggregate after executing business actions.
- You remove an aggregate that is no longer needed.
The critical distinction: a repository does not speak SQL, table columns, or document IDs.
It speaks purely in the language of the domain.
The Ports and Adapters Architecture
To understand how repositories interact with storage systems, we look to Hexagonal Architecture (also called Ports and Adapters).
Hexagonal Architecture divides the system into two distinct spaces:
- The Inside: Pure business logic (aggregates, value objects, domain services).
- The Outside: Technical infrastructure (databases, web controllers, message queues, third-party APIs).
The Port
A port is an interface defined by the domain.
It declares what the domain needs to do its job, without caring how it is achieved.
For example, an OrderRepository interface belongs inside the domain layer. It defines methods like save(order) and findById(orderId).
The Adapter
An adapter is the concrete implementation that lives in the infrastructure layer.
It translates the generic domain requests into concrete technical calls:
- A PostgreSQL adapter writes SQL statements.
- A MongoDB adapter serializes JSON documents.
- An in-memory adapter stores objects in a simple hash map.
Because the domain only depends on the port interface, the technical dependencies point inward. The database depends on the domain, not the other way around.
Rules of Thumb for Repositories
Applying repositories effectively requires a few practical principles:
One Repository per Aggregate Root
Repositories exist only for aggregate roots, never for individual entities or value objects.
If an Order aggregate contains OrderLine items, you do not create an OrderLineRepository.
You load and save the entire Order aggregate as a single consistency boundary.
Collection-Oriented Mindset
Design the repository interface like a collection, not a database query helper.
Methods should reflect domain needs (findPendingOrders(), save()), rather than technical SQL operations (updateStatusWhereIdEquals()).
Keep Business Logic in the Aggregate
A common anti-pattern is moving domain calculations into repository queries.
The repository’s only job is to load and persist state. The aggregate remains responsible for validating business invariants.
Decouple Testing from Databases
Because the repository is defined as a port, unit tests can use lightweight in-memory adapters.
Tests run in milliseconds without requiring external database containers or network fixtures.
Example: Managing Orders
Consider an e-commerce platform:
- The Order aggregate enforces rules: items must be valid, discounts cannot exceed maximum limits, and paid orders cannot be modified.
- The OrderRepository port defines how orders are retrieved and stored:
findById(orderId)andsave(order). - When a customer checks out, the application loads the
Orderaggregate via the repository, invokesorder.markPaid(), and saves the aggregate back. - In production, a database adapter handles relational table writes. In unit tests, an in-memory adapter handles the operation instantaneously.
Neither the Order aggregate nor the business use case contains a single line of SQL.
Closing Thoughts
Repositories complete the foundation of aggregate lifecycle management in Domain-Driven Design.
By acting as virtual collections, they free our models from the gravity of database schemas.
And by framing repositories as ports in a hexagonal architecture, we ensure our digital structures remain resilient to technical churn: databases and frameworks can change, but core business rules remain intact.
With aggregates, domain events, and repositories in place, one key tactical question remains:
Where do we place business logic that spans multiple aggregates or requires use-case orchestration?
In our next article, we will explore Domain Services and Application Services.