Skip to main content

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

ConceptDefinition
FactoryEncapsulates complex object creation
When to UseConstruction is complex or requires domain knowledge
TypesFactory methods, factory classes, abstract factories
LocationDomain 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:

  1. Complex creation logic - Validation, defaults, derived values
  2. Multiple creation scenarios - Create(), Import(), FromQuote()
  3. Domain events on creation - Factory can emit OrderCreated
  4. External dependencies - Factory class can inject services
  5. 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