Explain the difference between Factories and Repositories in Domain-Driven Design (DDD). (Senior Level Developer)
Question
Explain the difference between Factories and Repositories in Domain-Driven Design (DDD). (Senior Level Developer)
Brief Answer
In Domain-Driven Design (DDD), Factories and Repositories are distinct but complementary patterns, crucial for maintaining a clean, robust, and testable domain model.
Core Distinction: Creation vs. Persistence/Retrieval
- Factories: Their primary role is to encapsulate the complex logic required to create new domain objects, particularly Aggregates. They ensure that any newly created instance is valid, consistent, and adheres to all business invariants from the moment of its inception. Think of them as dedicated builders for complex domain objects, handling their initial state and dependencies.
- Repositories: These patterns are responsible for abstracting the concerns of data persistence and retrieval for *existing* domain objects (Entities and Aggregates). They provide a collection-like interface (e.g.,
GetById,Add,Update,Remove) that allows the domain to interact with stored objects without knowing the underlying data storage mechanism (database, file system, etc.).
Key Benefits & Strategic Value:
- Separation of Concerns: This clear division ensures the domain model focuses purely on business logic and behavior, free from the complexities of object construction or data access.
- Domain Integrity: Factories guarantee objects are created in a valid state, while Repositories ensure Aggregates are loaded and saved as a consistent whole, preserving their boundaries and invariants.
- Testability & Maintainability: Both promote interface-based design, enabling loose coupling and easy mocking for unit tests. Repositories, in particular, allow swapping persistence technologies without impacting the domain logic.
Senior Hint: While essential for complex object creation, a Factory might be overkill for simple Value Objects or Entities with trivial construction. Their strategic value becomes apparent when enforcing complex invariants or coordinating multiple dependencies during the “birth” of an Aggregate.
Super Brief Answer
In DDD, Factories create *new* domain objects (especially Aggregates), encapsulating complex construction logic and ensuring their initial validity. Repositories manage *existing* domain objects (Entities and Aggregates) by abstracting data persistence and retrieval, providing a collection-like interface to the underlying data store. Simply put: Factories handle object creation; Repositories handle their lifecycle management (load, save, delete).
Detailed Answer
In Domain-Driven Design (DDD), Factories and Repositories are distinct patterns that support a clean, maintainable domain model. Factories are responsible for encapsulating the complex logic required to create new domain objects, especially Aggregates, ensuring they are valid and consistent from their inception. In contrast, Repositories abstract the concerns of data persistence and retrieval, providing a collection-like interface for accessing existing domain objects and saving changes to the underlying data store.
Simply put: Factories create objects; Repositories retrieve and persist them.
While both are crucial building blocks in DDD, their primary responsibilities are fundamentally different, leading to a robust separation of concerns.
Core Distinction:
Factories encapsulate complex object creation logic, especially for Aggregates, ensuring new instances are valid and consistent. Repositories abstract data access for Entities and Aggregates, providing a collection-like interface for retrieving, saving, and deleting existing domain objects. They decouple creation and persistence concerns from the domain itself.
Key Concepts:
This discussion is related to: Domain Services, Factories, Repositories, Entities, Aggregates, Value Objects, Invariants.
Deep Dive into Key Differences:
1. Distinct Responsibilities: Creation vs. Persistence Retrieval
This separation ensures that the domain logic remains focused on business rules and behavior, rather than the technical details of object creation or persistence. Factories handle the intricacies of object construction, including satisfying invariants and dependencies. Repositories, on the other hand, handle the loading and storing of objects, abstracting away the underlying data access mechanism. This clear separation of concerns enhances maintainability and testability.
2. Abstraction: Hiding Complexities
Factories simplify object creation by providing a single point of entry for creating complex objects. This hides the internal construction logic and dependencies from the client code. Repositories simplify data access by providing a collection-like interface to retrieve and persist domain objects. This hides the underlying database interaction specifics, allowing the domain logic to remain independent of the persistence mechanism. This abstraction simplifies the domain logic and makes it easier to reason about. For instance, if the underlying database changes, only the repository needs to be updated, not the domain logic.
3. Domain Focus: Bringing New Objects to Life vs. Dealing with Existing
Repositories focus on retrieving and persisting existing domain objects, acting as a gateway to the persistent store. They provide methods like GetById, FindAll, etc., similar to working with a collection. Factories, conversely, focus on creating new instances of domain objects, ensuring that these new objects are valid and comply with business rules. This clear distinction in their roles helps to maintain a clean separation of concerns within the domain model.
4. Aggregate Roots: Their Connection to Aggregates
Aggregate Roots act as consistency boundaries in DDD. Factories are essential in creating Aggregate Roots because they ensure that all invariants and consistency rules of the Aggregate are met upon creation. Since an Aggregate Root is retrieved by its ID, the Repository retrieves the entire Aggregate, ensuring that access to the Aggregate’s internal entities is controlled through the root. This helps maintain the integrity of the Aggregate. For example, if an Order is an Aggregate Root, the Order Factory would be responsible for creating the Order and its associated Order Items, ensuring that the order total is calculated correctly. The Order Repository would then retrieve the entire Order Aggregate by its ID.
5. Interface-Based Design: Promoting Loose Coupling and Testability
Interfaces enable loose coupling by decoupling the domain logic from the concrete implementation of both Factories and Repositories. This is crucial for testability because it allows for easy mocking of these dependencies in unit tests. Interfaces also enhance maintainability by allowing for easy swapping of implementations without affecting the domain logic. For instance, you could easily switch from an in-memory repository to a database-backed repository during development. This flexibility and separation are vital for long-term project health.
Interview Hints for Senior Developers:
1. Show Strategic Value
Show a deep understanding of the strategic value of Factories and Repositories in DDD. Don’t just define them; explain why they are important. Factories and Repositories are essential building blocks in DDD because they help to achieve a clean separation of concerns, maintain domain integrity, and improve testability and maintainability. They contribute to a more modular and maintainable codebase by isolating the complexities of object creation and persistence from the core domain logic. This allows developers to focus on the business rules and behavior, rather than the technical details of how objects are created or stored.
2. Use Real-World Examples
Use real-world examples to illustrate how they’re used in complex domains. Talk about how they contribute to a cleaner, more maintainable architecture. For example, explain how a Factory can enforce business rules during object creation or how a Repository simplifies querying and filtering data. Imagine an e-commerce platform. An Order Factory could encapsulate the logic for creating an Order, ensuring that an order must have at least one item and that discounts are applied correctly. This enforces business rules at the point of object creation. The Order Repository would provide a simple interface for retrieving orders based on various criteria, like customer ID or order date, abstracting away the complexities of database queries. This makes the code cleaner and easier to maintain.
3. Demonstrate Nuanced Thinking
Mention scenarios where you might not need a Factory (simple object creation) or might prefer a different pattern. Demonstrate nuanced thinking. If object creation is straightforward and involves no complex logic or invariants, a Factory might be overkill. For instance, creating a simple Value Object like an Address might not require a Factory. In scenarios involving object creation based on complex conditional logic or configurations, the Builder pattern or Abstract Factory might be more appropriate. Recognizing these nuances demonstrates a deeper understanding of design patterns.
4. Connect to Persistence Technologies
If you have experience with different persistence technologies (SQL, NoSQL, ORMs), briefly discuss how Repositories can abstract these differences. Repositories act as an abstraction layer over the underlying persistence technology. Whether you’re using SQL Server, MongoDB, or an ORM like Entity Framework, the domain logic interacts with the Repository interface, oblivious to the specific database being used. This allows for flexibility in choosing or changing the persistence technology without impacting the domain model. For example, you can switch from a SQL-based repository to a NoSQL-based repository without changing the domain logic, only the repository implementation needs to be adapted.
Code Sample:
Below is a C# example demonstrating the usage of IOrderRepository and OrderFactory within a DDD context, showing how they contribute to a clean architecture.
// Example Interface for a Repository
public interface IOrderRepository
{
Order GetById(Guid id);
void Add(Order order);
void Update(Order order);
void Remove(Order order);
// Maybe methods for finding orders by customer, date range, etc.
IEnumerable<Order> FindByCustomerId(Guid customerId);
}
// Example Implementation (abstracting EF Core)
public class EfCoreOrderRepository : IOrderRepository
{
private readonly ApplicationDbContext _context;
public EfCoreOrderRepository(ApplicationDbContext context)
{
_context = context;
}
public Order GetById(Guid id)
{
// Abstracting database query details
return _context.Orders
.Include(o => o.OrderItems) // Ensure Aggregate consistency
.FirstOrDefault(o => o.Id == id);
}
public void Add(Order order)
{
_context.Orders.Add(order);
}
public void Update(Order order)
{
_context.Orders.Update(order);
}
public void Remove(Order order)
{
_context.Orders.Remove(order);
}
public IEnumerable<Order> FindByCustomerId(Guid customerId)
{
return _context.Orders
.Include(o => o.OrderItems)
.Where(o => o.CustomerId == customerId)
.ToList();
}
}
// Example Factory for creating an Order Aggregate
public static class OrderFactory
{
public static Order CreateNewOrder(Guid customerId, IEnumerable<OrderItemData> itemData)
{
// Enforce invariants and complex creation logic here
if (itemData == null || !itemData.Any())
{
throw new DomainException("An order must have at least one item.");
}
var orderItems = itemData.Select(data => new OrderItem(data.ProductId, data.Quantity, data.UnitPrice)).ToList();
// Calculate total, apply discounts, etc.
var newOrder = new Order(Guid.NewGuid(), customerId, DateTime.UtcNow, orderItems);
// Ensure Aggregate invariants are met before returning
newOrder.CalculateTotal(); // Example of complex logic encapsulated
return newOrder;
}
}
// Example Usage in a Domain Service or Application Service
public class OrderService
{
private readonly IOrderRepository _orderRepository;
public OrderService(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public void PlaceOrder(Guid customerId, IEnumerable<OrderItemData> items)
{
// Use the Factory to create a valid Order Aggregate
var newOrder = OrderFactory.CreateNewOrder(customerId, items);
// Use the Repository to persist the Aggregate
_orderRepository.Add(newOrder);
// Unit of Work would handle saving changes here in a real app
// _unitOfWork.Commit();
}
public Order GetOrderDetails(Guid orderId)
{
// Use the Repository to retrieve the Aggregate
var order = _orderRepository.GetById(orderId);
if (order == null)
{
throw new ApplicationException($"Order with ID {orderId} not found.");
}
return order;
}
}
// Simple data classes (often Value Objects or DTOs)
public class OrderItemData
{
public Guid ProductId { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
// Domain Exceptions
public class DomainException : Exception
{
public DomainException(string message) : base(message) { }
}

