DDD Repositories
Repository
In Domain-Driven Design (DDD), a Repository is a design pattern used to abstract the retrieval and persistence of aggregates and entities from a data source, such as a database, in a way that aligns with the domain model. Repositories act as a mediator between the domain layer and the data layer, providing a collection-like interface for accessing aggregates while hiding the underlying complexity of data access.
Repositories are typically used for aggregate roots. In DDD, aggregates encapsulate clusters of entities, and only aggregate roots should be retrieved or persisted directly via repositories.
Key Characteristics
-
Abstraction: Repositories abstract away the details of data storage, whether it's a relational database, NoSQL store, or any other persistence mechanism. The domain model is decoupled from how and where data is stored.
-
Collection-like Interface: Repositories provide methods to add, remove, and retrieve entities as if working with an in-memory collection of objects.
-
Aggregate Persistence: Repositories are typically responsible for loading and saving entire aggregates, ensuring that all related entities are persisted together, maintaining the integrity of the aggregate.
-
Repository Interface: In DDD, repositories often expose a standard set of methods such as:
add(): To add a new entity.getById(): To retrieve an entity by its ID.remove(): To delete an entity.- Other query methods to retrieve entities based on specific criteria.
Example
Let’s extend our e-commerce example to include a repository for the Order aggregate.
1. Defining a Repository Interface
We’ll start by defining an OrderRepository interface, which provides the operations for retrieving and saving Order aggregates.
interface OrderRepository {
add(order: Order): Promise<void>;
getById(orderId: string): Promise<Order | null>;
remove(order: Order): Promise<void>;
}This is a typical repository interface:
add()saves a newOrderto the persistence layer.getById()retrieves anOrderby its ID.remove()deletes an existingOrder.
2. In-Memory Implementation of the Repository
Let’s implement this repository interface using an in-memory data store. In a real-world application, this could be connected to a database such as PostgreSQL or MongoDB, but for simplicity, we’ll use an in-memory array.
class InMemoryOrderRepository implements OrderRepository {
private orders: Map<string, Order> = new Map();
async add(order: Order): Promise<void> {
this.orders.set(order.orderId, order);
}
async getById(orderId: string): Promise<Order | null> {
return this.orders.get(orderId) || null;
}
async remove(order: Order): Promise<void> {
this.orders.delete(order.orderId);
}
}Here, we use a Map to store orders by their orderId. This is a simple in-memory storage mechanism, but the concept would be the same if this were backed by a database.
3. Using the Repository
Let’s see how we might use the repository in our domain logic. We’ll use the OrderRepository to add and retrieve Order objects.
async function placeNewOrder(orderRepository: OrderRepository, customer: Customer, products: Product[], quantities: number[]) {
// Create a new order using the factory (which handles order creation logic)
const orderFactory = new OrderFactory();
const order = orderFactory.createOrder(customer, products, quantities);
await orderRepository.add(order);
console.log(`Order ${order.orderId} has been placed for customer ${customer.customerId}`);
}
async function getOrderDetails(orderRepository: OrderRepository, orderId: string) {
const order = await orderRepository.getById(orderId);
if (!order) {
console.log(`Order ${orderId} not found.`);
return;
}
console.log(`Order ${orderId} found with total amount: $${order.getTotalAmount()}`);
}
// Example usage:
const orderRepository = new InMemoryOrderRepository();
const customer = new Customer('customer123', 'Alice');
const product1 = new Product('product1', 50, 10); // Product with 10 units in stock
const product2 = new Product('product2', 100, 5); // Product with 5 units in stock
await placeNewOrder(orderRepository, customer, [product1, product2], [2, 1]);
await getOrderDetails(orderRepository, 'order123');4. Querying the Repository
Repositories typically also provide methods for querying based on business rules. For example, you could add a method to retrieve all orders for a given customer:
interface OrderRepository {
add(order: Order): Promise<void>;
getById(orderId: string): Promise<Order | null>;
remove(order: Order): Promise<void>;
// New query method
getOrdersByCustomerId(customerId: string): Promise<Order[]>;
}
// In-memory implementation of the new query method
class InMemoryOrderRepository implements OrderRepository {
// ...
async getOrdersByCustomerId(customerId: string): Promise<Order[]> {
return Array.from(this.orders.values()).filter(order => order.customerId === customerId);
}
}This method, getOrdersByCustomerId, allows you to retrieve all orders placed by a specific customer, which could be useful for generating customer reports or order history.
Benefits of Using Repositories
-
Separation of Concerns: The domain layer focuses on business logic, while the repository layer handles persistence. This decoupling makes the domain layer cleaner and easier to test.
-
Testability: By using repositories, you can easily mock or replace the repository in tests, allowing for testing of domain logic without needing access to a database.
-
Flexibility: The repository provides an abstraction over data access. If you decide to switch from one database to another (e.g., from MongoDB to PostgreSQL), you can do so without affecting the domain model.
-
Consistency and Integrity: Repositories enforce consistency by ensuring that aggregates are loaded, saved, and removed as whole units. This maintains the integrity of the data.
Conclusion
In Domain-Driven Design, repositories provide an abstraction for accessing and persisting domain aggregates and entities, separating the domain logic from the details of data storage. They simplify the domain model by abstracting away persistence concerns and offer a collection-like interface for retrieving and manipulating aggregates.
Next
Visit Services to dive deeper into Domain Driven Design.
Check next:
READ
Latest readings
Readings are sites which will help you with detailed
information about given topic. Read latest ones from Learn.
06-03-2026
Build your own local voice assistant powered by Ollama.
06-03-2026
Generate YouTube thumbnails with FastAPI and Ollama.
05-09-2024
Compare Neo4j and Tigergraph databases, which is easier to work with, etc.