Factories: Complex Object Creation
Factories encapsulate the creation of complex aggregates and entities. When object construction involves significant logic, validation, or coordination, factories keep that complexity out of application code.
TL;DR
| Concept | Definition |
|---|---|
| Factory | Encapsulates complex object creation |
| When to Use | Construction is complex or requires domain knowledge |
| Types | Factory methods, factory classes, abstract factories |
| Location | Domain layer, often on aggregate root |
Why Factories?
Without Factory
// ❌ Complex creation scattered in application code
public class OrderApplicationService
{
public async Task<OrderId> CreateOrder(CreateOrderCommand command)
{
var orderId = OrderId.New();
var order = new Order();
order.Id = orderId;
order.CustomerId = command.CustomerId;
order.Status = OrderStatus.Draft;
order.CreatedAt = DateTime.UtcNow;
order.ShippingAddress = command.ShippingAddress;
order.BillingAddress = command.BillingAddress ?? command.ShippingAddress;
foreach (var item in command.Items)
{
var line = new OrderLine();
line.Id = OrderLineId.New();
line.ProductId = item.ProductId;
line.Quantity = item.Quantity;
line.UnitPrice = await _pricingService.GetPrice(item.ProductId);
order.Lines.Add(line);
}
order.Total = order.Lines.Sum(l => l.UnitPrice * l.Quantity);
// More initialization...
}
}
Problems:
- Creation logic duplicated across services
- Domain knowledge scattered
- Easy to create invalid objects
- Hard to maintain consistency
With Factory
// ✅ Factory encapsulates creation
public class Order
{
// Factory method on aggregate root
public static Order Create(
CustomerId customerId,
Address shippingAddress,
Address? billingAddress = null)
{
var order = new Order(
OrderId.New(),
customerId,
shippingAddress,
billingAddress ?? shippingAddress);
order.AddDomainEvent(new OrderCreated(order.Id, customerId));
return order;
}
// Private constructor - must use factory
private Order(
OrderId id,
CustomerId customerId,
Address shippingAddress,
Address billingAddress)
{
Id = id;
CustomerId = customerId;
ShippingAddress = shippingAddress;
BillingAddress = billingAddress;
Status = OrderStatus.Draft;
CreatedAt = DateTime.UtcNow;
Total = Money.Zero(Currency.USD);
}
}
// Application service is simple
public class OrderApplicationService
{
public async Task<OrderId> CreateOrder(CreateOrderCommand command)
{
var order = Order.Create(
command.CustomerId,
command.ShippingAddress,
command.BillingAddress);
await _orderRepository.Save(order);
return order.Id;
}
}
Factory Patterns
Pattern 1: Factory Method on Aggregate
Most common and recommended for simple cases:
public class Customer : AggregateRoot<CustomerId>
{
// Factory method
public static Customer Register(
EmailAddress email,
CustomerName name,
Address? address = null)
{
// Validation
if (email == null) throw new ArgumentNullException(nameof(email));
if (name == null) throw new ArgumentNullException(nameof(name));
var customer = new Customer(
CustomerId.New(),
email,
name,
address,
CustomerStatus.Active,
LoyaltyTier.Bronze);
customer.AddDomainEvent(new CustomerRegistered(customer.Id, email));
return customer;
}
// Another factory method for different creation scenario
public static Customer Import(
CustomerId id,
EmailAddress email,
CustomerName name,
CustomerStatus status,
LoyaltyTier tier)
{
// For importing from external system
return new Customer(id, email, name, null, status, tier);
}
// Private constructor
private Customer(
CustomerId id,
EmailAddress email,
CustomerName name,
Address? address,
CustomerStatus status,
LoyaltyTier tier) : base(id)
{
Email = email;
Name = name;
Address = address;
Status = status;
LoyaltyTier = tier;
}
}
Pattern 2: Separate Factory Class
For complex creation that needs external dependencies:
public interface IOrderFactory
{
Task<Order> CreateFromCart(ShoppingCart cart, Address shippingAddress);
Task<Order> CreateFromQuote(Quote quote);
}
public class OrderFactory : IOrderFactory
{
private readonly IPricingService _pricingService;
private readonly IInventoryService _inventoryService;
private readonly ICustomerRepository _customerRepository;
public OrderFactory(
IPricingService pricingService,
IInventoryService inventoryService,
ICustomerRepository customerRepository)
{
_pricingService = pricingService;
_inventoryService = inventoryService;
_customerRepository = customerRepository;
}
public async Task<Order> CreateFromCart(
ShoppingCart cart,
Address shippingAddress)
{
var customer = await _customerRepository.GetById(cart.CustomerId)
?? throw new CustomerNotFoundException(cart.CustomerId);
var order = Order.Create(customer.Id, shippingAddress);
foreach (var item in cart.Items)
{
// Check inventory
var available = await _inventoryService.CheckAvailability(
item.ProductId,
item.Quantity);
if (!available)
throw new InsufficientInventoryException(item.ProductId);
// Get customer-specific pricing
var price = await _pricingService.GetPrice(
item.ProductId,
customer,
item.Quantity);
order.AddLine(item.ProductId, item.Quantity, price);
}
return order;
}
public async Task<Order> CreateFromQuote(Quote quote)
{
if (quote.Status != QuoteStatus.Accepted)
throw new QuoteNotAcceptedException(quote.Id);
if (quote.ExpiresAt < DateTime.UtcNow)
throw new QuoteExpiredException(quote.Id);
var order = Order.Create(
quote.CustomerId,
quote.ShippingAddress);
// Use quoted prices (locked in)
foreach (var item in quote.Items)
{
order.AddLine(item.ProductId, item.Quantity, item.QuotedPrice);
}
order.ApplyQuoteDiscount(quote.Discount);
return order;
}
}
Pattern 3: Builder Pattern
For aggregates with many optional parameters:
public class OrderBuilder
{
private CustomerId _customerId;
private Address _shippingAddress;
private Address? _billingAddress;
private readonly List<(ProductId, int, Money)> _lines = new();
private Money? _discount;
private string? _notes;
private bool _isGift;
private string? _giftMessage;
public OrderBuilder ForCustomer(CustomerId customerId)
{
_customerId = customerId;
return this;
}
public OrderBuilder ShipTo(Address address)
{
_shippingAddress = address;
return this;
}
public OrderBuilder BillTo(Address address)
{
_billingAddress = address;
return this;
}
public OrderBuilder AddLine(ProductId productId, int quantity, Money price)
{
_lines.Add((productId, quantity, price));
return this;
}
public OrderBuilder WithDiscount(Money discount)
{
_discount = discount;
return this;
}
public OrderBuilder WithNotes(string notes)
{
_notes = notes;
return this;
}
public OrderBuilder AsGift(string message)
{
_isGift = true;
_giftMessage = message;
return this;
}
public Order Build()
{
// Validation
if (_customerId == null)
throw new InvalidOperationException("Customer is required");
if (_shippingAddress == null)
throw new InvalidOperationException("Shipping address is required");
if (!_lines.Any())
throw new InvalidOperationException("Order must have at least one line");
var order = Order.Create(_customerId, _shippingAddress, _billingAddress);
foreach (var (productId, quantity, price) in _lines)
{
order.AddLine(productId, quantity, price);
}
if (_discount.HasValue)
order.ApplyDiscount(_discount.Value);
if (_notes != null)
order.AddNotes(_notes);
if (_isGift)
order.MarkAsGift(_giftMessage);
return order;
}
}
// Usage
var order = new OrderBuilder()
.ForCustomer(customerId)
.ShipTo(shippingAddress)
.AddLine(product1, 2, price1)
.AddLine(product2, 1, price2)
.WithDiscount(new Money(10, Currency.USD))
.AsGift("Happy Birthday!")
.Build();
Pattern 4: Reconstitution Factory
For loading aggregates from persistence:
public class Order : AggregateRoot<OrderId>
{
// Factory for creating new orders
public static Order Create(CustomerId customerId, Address shippingAddress)
{
// ... validation, events, etc.
}
// Factory for reconstituting from database
// No validation, no events - just rebuilding state
internal static Order Reconstitute(
OrderId id,
CustomerId customerId,
Address shippingAddress,
Address billingAddress,
OrderStatus status,
Money total,
DateTime createdAt,
IEnumerable<OrderLine> lines,
int version)
{
var order = new Order(id, customerId, shippingAddress, billingAddress)
{
Status = status,
Total = total,
CreatedAt = createdAt,
Version = version
};
foreach (var line in lines)
{
order._lines.Add(line);
}
return order;
}
}
// Repository uses reconstitution
public class OrderRepository : IOrderRepository
{
public async Task<Order?> GetById(OrderId id)
{
var data = await _context.Orders
.Include(o => o.Lines)
.FirstOrDefaultAsync(o => o.Id == id);
if (data == null) return null;
return Order.Reconstitute(
data.Id,
data.CustomerId,
data.ShippingAddress,
data.BillingAddress,
data.Status,
data.Total,
data.CreatedAt,
data.Lines.Select(l => OrderLine.Reconstitute(...)),
data.Version);
}
}
When to Use Factories
┌─────────────────────────────────────────────────────────┐
│ FACTORY DECISION GUIDE │
├─────────────────────────────────────────────────────────┤
│ │
│ Use Factory Method on Aggregate When: │
│ • Creation is straightforward │
│ • No external dependencies needed │
│ • Single way to create │
│ │
│ Use Separate Factory Class When: │
│ • Creation needs external services │
│ • Multiple creation scenarios │
│ • Complex coordination required │
│ • Need to inject dependencies │
│ │
│ Use Builder When: │
│ • Many optional parameters │
│ • Complex configuration │
│ • Want fluent API │
│ │
│ Use Reconstitution Factory When: │
│ • Loading from database │
│ • Deserializing from events │
│ • No validation needed (data already valid) │
│ │
└─────────────────────────────────────────────────────────┘
Common Mistakes
Mistake 1: Public Constructors Alongside Factories
// ❌ Can bypass factory
public class Order
{
public Order() { } // Public default constructor
public static Order Create(...) { ... }
}
// Anyone can do this:
var order = new Order(); // Invalid state!
// ✅ Private constructor forces factory use
public class Order
{
private Order() { }
public static Order Create(...) { ... }
}
Mistake 2: Factory Creating Invalid Objects
// ❌ Factory doesn't ensure validity
public static Order Create(CustomerId customerId)
{
return new Order
{
CustomerId = customerId
// Missing required fields!
};
}
// ✅ Factory ensures validity
public static Order Create(
CustomerId customerId,
Address shippingAddress)
{
if (customerId == null) throw new ArgumentNullException(nameof(customerId));
if (shippingAddress == null) throw new ArgumentNullException(nameof(shippingAddress));
return new Order(
OrderId.New(),
customerId,
shippingAddress,
OrderStatus.Draft,
DateTime.UtcNow);
}
Mistake 3: Too Much Logic in Factory
// ❌ Factory doing too much
public class OrderFactory
{
public async Task<Order> Create(CreateOrderCommand cmd)
{
var order = Order.Create(...);
// This is application service logic!
await _inventoryService.Reserve(order.Lines);
await _paymentService.Authorize(order.Total);
await _emailService.SendConfirmation(order);
return order;
}
}
// ✅ Factory only creates, application service orchestrates
public class OrderFactory
{
public async Task<Order> Create(CreateOrderCommand cmd)
{
var order = Order.Create(cmd.CustomerId, cmd.ShippingAddress);
foreach (var item in cmd.Items)
{
var price = await _pricingService.GetPrice(item.ProductId);
order.AddLine(item.ProductId, item.Quantity, price);
}
return order; // Just return the created object
}
}
Staff+ Interview Questions
Q: When would you use a factory vs constructor?
A: Use factory when:
- Complex creation logic - Validation, defaults, derived values
- Multiple creation scenarios -
Create(),Import(),FromQuote() - Domain events on creation - Factory can emit
OrderCreated - External dependencies - Factory class can inject services
- Encapsulation - Hide construction details
Use simple constructor when creation is trivial with no special logic.
Q: Should factories validate business rules?
A: Factories should validate creation invariants - rules that must be true for a valid object to exist. They shouldn't validate operation invariants - rules about what operations are allowed.
// Factory validates creation invariants
public static Order Create(CustomerId customerId, Address address)
{
if (customerId == null) throw new ArgumentNullException(); // Creation invariant
if (address == null) throw new ArgumentNullException(); // Creation invariant
// ...
}
// Aggregate validates operation invariants
public void Ship()
{
if (Status != OrderStatus.Placed) // Operation invariant
throw new InvalidOperationException();
}
Quick Reference Card
┌─────────────────────────────────────────────────────────┐
│ FACTORIES QUICK REFERENCE │
├─────────────────────────────────────────────────────────┤
│ │
│ PURPOSE │
│ Encapsulate complex object creation │
│ │
│ PATTERNS │
│ • Factory Method: Static method on aggregate │
│ • Factory Class: Separate class with dependencies │
│ • Builder: Fluent API for many options │
│ • Reconstitution: Loading from persistence │
│ │
│ GUIDELINES │
│ • Make constructors private │
│ • Validate creation invariants │
│ • Emit domain events when appropriate │
│ • Keep factory focused on creation │
│ │
│ AVOID │
│ • Public constructors alongside factories │
│ • Creating invalid objects │
│ • Application logic in factories │
│ │
└─────────────────────────────────────────────────────────┘
Next Steps
- DDD + Event Sourcing - Factories with event sourcing
- Aggregates - What factories create
- Application Services - Using factories