Aggregate Design Patterns That Actually Work cover image

Aggregate Design Patterns That Actually Work

By Julian Krause · Wednesday, February 5, 2025 · ~3 min read

Aggregate design is where DDD theory meets the harsh reality of production systems. I have seen teams either make aggregates too large — turning them into god objects that lock entire tables — or too small, losing the transactional guarantees that make aggregates useful in the first place. After years of getting it wrong and occasionally getting it right, here are patterns that have proven reliable.

Keep Aggregates Small

The single most impactful rule is to keep aggregates small. An aggregate should protect a single consistency boundary, not model an entire business concept. When you feel the urge to add another entity to your aggregate, ask yourself: does this really need to be transactionally consistent with the root?

// Too large — Order contains everything
public class Order
{
    public OrderId Id { get; private set; }
    public Customer Customer { get; private set; }
    public List<OrderLine> Lines { get; private set; }
    public List<Payment> Payments { get; private set; }
    public ShippingInfo Shipping { get; private set; }
    public List<Invoice> Invoices { get; private set; } // Does this really belong here?
}

// Better — Order focuses on the ordering invariant
public class Order
{
    public OrderId Id { get; private set; }
    public CustomerId CustomerId { get; private set; } // Reference by ID
    private readonly List<OrderLine> _lines = [];

    public IReadOnlyCollection<OrderLine> Lines => _lines.AsReadOnly();

    public void AddLine(ProductId productId, int quantity, Money unitPrice)
    {
        if (quantity <= 0)
            throw new DomainException("Quantity must be positive.");

        var existing = _lines.FirstOrDefault(l => l.ProductId == productId);
        if (existing is not null)
            existing.IncreaseQuantity(quantity);
        else
            _lines.Add(new OrderLine(productId, quantity, unitPrice));
    }
}

Reference Other Aggregates by Identity

This is a corollary to keeping aggregates small. When one aggregate needs to reference another, use the identity value, not a direct object reference. This prevents unintended lazy loading, keeps serialization clean, and makes it obvious that the referenced entity lives outside the current consistency boundary.

public record OrderId(Guid Value);
public record CustomerId(Guid Value);

public class Order
{
    public OrderId Id { get; private set; }
    public CustomerId CustomerId { get; private set; } // Not Customer Customer

    // The order doesn't need to know the customer's name to enforce its own invariants
}

Use Domain Events for Cross-Aggregate Side Effects

When an action on one aggregate should trigger behavior in another, domain events are the right tool. They decouple the aggregates while making the causal relationship explicit.

public class Order
{
    private readonly List<IDomainEvent> _domainEvents = [];
    public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();

    public void Confirm()
    {
        if (Status != OrderStatus.Pending)
            throw new DomainException("Only pending orders can be confirmed.");

        Status = OrderStatus.Confirmed;
        _domainEvents.Add(new OrderConfirmedEvent(Id, CustomerId, TotalAmount));
    }

    public void ClearDomainEvents() => _domainEvents.Clear();
}

The Invariant Test

Before adding a rule to an aggregate, apply this test: if this rule is violated, is the system in an invalid state that could cause data corruption or financial loss? If yes, it belongs in the aggregate. If it is merely inconvenient or a soft business preference, handle it elsewhere — in an application service or a policy.

This distinction keeps your aggregates focused and your domain model honest. The best aggregate designs I have worked with enforce three to five invariants at most. If you find yourself with dozens, your aggregate is doing too much.


Comments (2)

Hannah Wednesday, February 18, 2026 5:08 PM

I have a question: does this also apply to older versions?

Ben Thursday, March 12, 2026 8:08 PM

Good question, I'd like to know too.

Clara Friday, March 13, 2026 8:08 PM

Interesting thought, thanks for adding that.

Anna Thursday, February 19, 2026 5:08 PM

Thanks for sharing — very helpful.

Leave a Comment