# Factory Pattern in Domain-Driven Design With C#

> Factories encapsulate complex object creation logic in DDD. Here is when and how to use factories for creating aggregates, entities, and value objects in C#.

Published: 2026-09-22. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/factory-pattern-domain-driven-design

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**](https://milanjovanovic.tech/blog/domain-driven-design-dotnet-getting-started), creating an [**aggregate**](https://milanjovanovic.tech/blog/aggregate-root-ddd) often involves:

- Validating [**business rules**](https://milanjovanovic.tech/blog/domain-invariants-business-rules)
- Setting default values
- Creating child entities
- Raising [**domain events**](https://milanjovanovic.tech/blog/how-to-use-domain-events-to-build-loosely-coupled-systems)

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:

```csharp
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:

```csharp
// 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:

```csharp
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**](https://milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals) also benefit from factory methods:

```csharp
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`](https://learn.microsoft.com/en-us/dotnet/api/system.net.mail.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**](https://milanjovanovic.tech/blog/encapsulating-collections-domain-entities) to keep full control of the children:

```csharp
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:

```csharp
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:

```csharp
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:

1. **Static factory methods** - use for most aggregates and value objects
2. **Separate factory classes** - use when creation needs external services or async
3. **Aggregate as factory** - aggregates create their child entities
4. **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.
