A factory in Domain-Driven Design creates an aggregate or value object while enforcing the rules required for its initial state. Use a static factory method for construction within one model and a separate factory when creation needs collaborators or asynchronous data. Keep domain validation inside the object even when a factory coordinates its creation.
Why Factories?
In Domain-Driven Design, creating an aggregate often involves:
- Validating business rules
- Setting default values
- Creating child entities
- Raising domain events
When construction logic gets complex, it shouldn't live in the calling code. That's where factories come in.
Factory Methods on the Aggregate
The simplest factory pattern in DDD is a static factory method on the aggregate root:
In Order.cs, accept already-priced domain input and validate every line.
These excerpts use application-defined AggregateRoot, LineItem, event, and clock abstractions; Money is implemented below and this order uses USD:
public class Order : AggregateRoot
{
private readonly List<LineItem> _lineItems = [];
private Order() { } // EF Core constructor
public Guid CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
public Money TotalAmount { get; private set; } = Money.Zero();
public DateTime CreatedAt { get; private set; }
public IReadOnlyCollection<LineItem> LineItems => _lineItems.AsReadOnly();
public static Order Create(
Guid customerId,
IReadOnlyCollection<PricedItem> items,
IDateTimeProvider dateTimeProvider)
{
ArgumentNullException.ThrowIfNull(items);
ArgumentNullException.ThrowIfNull(dateTimeProvider);
if (customerId == Guid.Empty)
throw new DomainException("Customer ID is required.");
if (items.Count == 0)
throw new DomainException("Order must have at least one item.");
var order = new Order
{
Id = Guid.NewGuid(),
CustomerId = customerId,
Status = OrderStatus.Draft,
CreatedAt = dateTimeProvider.UtcNow
};
foreach (var item in items)
{
order.AddLineItem(item.ProductId, item.Quantity, item.UnitPrice);
}
order.RecalculateTotal();
order.RaiseDomainEvent(new OrderCreatedDomainEvent(order.Id));
return order;
}
private void AddLineItem(Guid productId, int quantity, decimal unitPrice)
{
if (productId == Guid.Empty)
throw new DomainException("Product ID is required.");
if (quantity <= 0)
throw new DomainException("Quantity must be positive.");
var lineItem = new LineItem(
Guid.NewGuid(), Id, productId, quantity, Money.Create(unitPrice));
_lineItems.Add(lineItem);
}
private void RecalculateTotal()
{
TotalAmount = Money.Create(
_lineItems.Sum(li => li.Price.Amount * li.Quantity));
}
}
public sealed record PricedItem(Guid ProductId, int Quantity, decimal UnitPrice);
public sealed record OrderItemRequest(Guid ProductId, int Quantity);
The Create method:
- Validates invariants
- Sets up the initial state
- Creates child entities
- Raises domain events
The calling code is simple:
// trustedPrices contains prices resolved by the application, not client input.
var order = Order.Create(command.CustomerId, trustedPrices, _dateTimeProvider);
_orderRepository.Add(order);
When to Use Separate Factory Classes
Sometimes construction logic is too complex for a static method - it needs external data or services:
Put this orchestration in the application layer when it performs repository and pricing-service calls. The aggregate still validates the resulting values:
public class OrderFactory
{
private readonly IProductRepository _productRepository;
private readonly IPricingService _pricingService;
private readonly IDateTimeProvider _dateTimeProvider;
public OrderFactory(
IProductRepository productRepository,
IPricingService pricingService,
IDateTimeProvider dateTimeProvider)
{
_productRepository = productRepository;
_pricingService = pricingService;
_dateTimeProvider = dateTimeProvider;
}
public async Task<Order> CreateAsync(
Guid customerId,
List<OrderItemRequest> items,
CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(items);
if (customerId == Guid.Empty || items.Count == 0)
throw new DomainException("A customer and at least one item are required.");
if (items.Any(item => item.ProductId == Guid.Empty || item.Quantity <= 0))
throw new DomainException("Each item needs a product and positive quantity.");
if (items.Select(item => item.ProductId).Distinct().Count() != items.Count)
throw new DomainException("Combine duplicate products before creating an order.");
// Look up current prices
var productIds = items.Select(i => i.ProductId).ToList();
var products = await _productRepository
.GetByIdsAsync(productIds, ct);
// Calculate prices with discounts
var pricedItems = new List<PricedItem>();
foreach (var item in items)
{
var product = products.SingleOrDefault(p => p.Id == item.ProductId)
?? throw new DomainException("Product was not found.");
if (product.Stock < item.Quantity)
throw new DomainException(
$"Insufficient stock for {product.Name}.");
var price = await _pricingService.CalculatePriceAsync(
product.Id, item.Quantity, ct);
pricedItems.Add(new PricedItem(
product.Id, item.Quantity, price));
}
return Order.Create(
customerId, pricedItems, _dateTimeProvider);
}
}
CalculatePriceAsync returns a decimal unit price in USD in this example.
Checking Stock here is advisory: another order can consume stock before this order is saved.
Reserve inventory with concurrency protection and an explicit failure policy before promising fulfillment.
Use a separate factory class when:
- You need to call repositories or external services
- You need async operations
- Creation coordinates several domain objects with an explicit persistence policy
- The creation algorithm is complex enough to warrant its own class
Factories for Value Objects
Value objects also benefit from factory methods:
public sealed record Email
{
private Email(string value) => Value = value;
public string Value { get; }
public static Email Create(string email)
{
if (string.IsNullOrWhiteSpace(email))
throw new DomainException("Email cannot be empty.");
var trimmed = email.Trim();
if (trimmed.Length > 254 ||
!System.Net.Mail.MailAddress.TryCreate(trimmed, out var address) ||
address.Address != trimmed)
throw new DomainException($"'{email}' is not a valid email.");
return new Email(trimmed);
}
}
public sealed record Money
{
private Money() { }
public decimal Amount { get; private init; }
public string Currency { get; private init; } = "USD";
public static Money Create(decimal amount, string currency = "USD")
{
if (amount < 0)
throw new DomainException("Amount cannot be negative.");
if (string.IsNullOrWhiteSpace(currency))
throw new DomainException("Currency is required.");
return new Money
{
Amount = amount,
Currency = currency.Trim().ToUpperInvariant()
};
}
public static Money Zero(string currency = "USD") =>
Create(0, currency);
}
Private constructors force callers to use Create, ensuring validation always runs.
Every public factory must delegate to that validation, including Money.Zero.
This money type accepts any nonblank currency code; validate against supported currencies if your domain needs that restriction.
MailAddress.TryCreate checks syntax, while a verification flow establishes mailbox ownership.
This policy preserves address casing and rejects display-name forms.
Aggregate Creating Child Entities
An aggregate root is often the factory for its child entities. This pairs with encapsulating collections to keep full control of the children:
public class ShoppingCart : AggregateRoot
{
private readonly List<CartItem> _items = [];
public IReadOnlyCollection<CartItem> Items => _items.AsReadOnly();
public void AddItem(Guid productId, int quantity, Money unitPrice)
{
if (productId == Guid.Empty || quantity <= 0)
throw new DomainException("A product and positive quantity are required.");
ArgumentNullException.ThrowIfNull(unitPrice);
var existingItem = _items.FirstOrDefault(
i => i.ProductId == productId);
if (existingItem is not null)
{
if (existingItem.UnitPrice != unitPrice)
throw new DomainException("Existing item has a different price.");
existingItem.IncreaseQuantity(quantity);
}
else
{
// Cart creates its own line items
var item = new CartItem(
Guid.NewGuid(), Id, productId, quantity, unitPrice);
_items.Add(item);
}
RaiseDomainEvent(new CartItemAddedDomainEvent(Id, productId, quantity));
}
}
The ShoppingCart controls how CartItem instances are created when the child has a non-public constructor and mutation methods.
An internal child API remains accessible to the whole domain assembly.
CartItem.IncreaseQuantity should use checked arithmetic and validate its input before assignment.
Aggregate Creating Another Aggregate
One aggregate can be a factory for another:
public class Auction : AggregateRoot
{
public Bid PlaceBid(Guid bidderId, Money amount)
{
ArgumentNullException.ThrowIfNull(amount);
if (bidderId == Guid.Empty)
throw new DomainException("Bidder ID is required.");
if (Status != AuctionStatus.Active)
throw new DomainException("Auction is not active.");
if (amount.Currency != CurrentHighBid.Currency)
throw new DomainException("Bid currency must match the auction.");
if (amount.Amount <= CurrentHighBid.Amount)
throw new DomainException(
"Bid must be higher than current bid.");
var bid = Bid.Create(Id, bidderId, amount);
CurrentHighBid = amount;
RaiseDomainEvent(new BidPlacedDomainEvent(Id, bid.Id, amount));
return bid;
}
}
The Auction validates the bid rules and creates the Bid - maintaining control over the business process.
The excerpt assumes the auction's factory initializes its status and starting amount.
If updating the auction and creating the bid must succeed together, persist them in one transaction and protect concurrent bids with a concurrency token or another atomic database operation.
Otherwise, define a pending-bid workflow; returning a new aggregate does not save it.
A Factory for Multiple Implementations
When you have different types of accounts or products:
public interface IAccountFactory
{
Account Create(string name, AccountType type);
}
public class AccountFactory : IAccountFactory
{
public Account Create(string name, AccountType type)
{
return type switch
{
AccountType.Checking => CheckingAccount.Create(name),
AccountType.Savings => SavingsAccount.Create(name, interestRate: 0.04m),
AccountType.Premium => PremiumAccount.Create(name, creditLimit: 10000m),
_ => throw new DomainException($"Unknown account type: {type}")
};
}
}
This is a simple factory that chooses an Account subtype.
The abstract factory pattern usually creates families of related products, so an interface and a switch alone do not establish that pattern.
When Should You Skip a Factory?
Skip factories when:
- Construction is straightforward (just setting properties)
- There are no invariants to validate
- No domain events to raise
- A simple constructor works fine
Don't add complexity where it's not needed.
Summary
Factories in DDD encapsulate object creation logic:
- Static factory methods - use for most aggregates and value objects
- Separate factory classes - use when creation needs external services or async
- Aggregate as factory - aggregates create their child entities
- Private constructors - force callers through the factory method
The goal is the same: ensure every aggregate is created in a valid state with all invariants satisfied.
Thanks for reading.
And stay awesome!
Frequently Asked Questions
What is the factory pattern in DDD?
In DDD, a factory encapsulates the logic of creating aggregates, entities, and value objects so that every object starts life in a valid state. It can be a static factory method on the aggregate or a separate factory class.
When should you use a factory class instead of a static factory method?
Use a separate factory class when creation needs collaborators or async lookups. Keep repository and external-service orchestration in the application layer and enforce the final invariants in the domain factory.
Why use a private constructor with a factory method in C#?
A private constructor routes ordinary callers through controlled factory methods. Every factory must enforce the same invariants; persistence and deserialization need their own mapping and validation policies.
Can an aggregate act as a factory for another aggregate?
Yes. When one aggregate controls the business process that produces another, like an Auction creating a Bid, putting the creation there keeps the rules in one place.
When is a factory unnecessary?
When construction just sets properties, there are no invariants to validate, and no domain events to raise. In that case a plain constructor is simpler and better.



