# Architecture Standard: DDD Aggregates

## Overview
Aggregates are the primary unit of consistency in DDD. An aggregate root is the only entry point.

## Design Rules
1. **Small aggregates**: Keep aggregates small (3-5 entities max)
2. **Reference by ID**: Aggregates reference each other by ID only (not navigation properties across aggregate boundaries)
3. **Consistency boundary**: All invariants within an aggregate must hold after every command
4. **Single repository**: One repository per aggregate root

## Aggregate Root Pattern
```csharp
public abstract class AggregateRoot : Entity
{
    private readonly List<DomainEvent> _domainEvents = [];

    public IReadOnlyList<DomainEvent> DomainEvents => _domainEvents.AsReadOnly();

    protected void RaiseDomainEvent(DomainEvent @event)
    {
        @event = @event with { Version = _domainEvents.Count + 1 };
        _domainEvents.Add(@event);
    }

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

public abstract class Entity
{
    public Guid Id { get; protected set; } = Guid.NewGuid();
    public DateTime CreatedAt { get; protected set; } = DateTime.UtcNow;
    public DateTime? UpdatedAt { get; protected set; }
}
```

## Order Aggregate Example
```csharp
public class Order : AggregateRoot
{
    private readonly List<OrderItem> _items = [];

    // Private constructor — use factory method
    private Order(Guid userId) { UserId = userId; Status = OrderStatus.Draft; }

    public Guid UserId { get; private set; }
    public OrderStatus Status { get; private set; }
    public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
    public decimal Total => _items.Sum(i => i.Subtotal);

    // Factory method — validates invariants before creating
    public static Order Create(Guid userId, IEnumerable<OrderItem> items)
    {
        if (userId == Guid.Empty) throw new DomainException("UserId required");
        var order = new Order(userId);
        foreach (var item in items) order.AddItem(item);
        order.RaiseDomainEvent(new OrderCreatedEvent(order.Id, userId));
        return order;
    }

    public void AddItem(OrderItem item)
    {
        if (Status != OrderStatus.Draft)
            throw new DomainException($"Cannot add items to order in {Status} status");

        var existing = _items.FirstOrDefault(i => i.ProductId == item.ProductId);
        if (existing != null)
            existing.IncreaseQuantity(item.Quantity);
        else
            _items.Add(item);

        UpdatedAt = DateTime.UtcNow;
    }

    public void Confirm()
    {
        if (Status != OrderStatus.Draft)
            throw new DomainException("Can only confirm draft orders");
        if (!_items.Any())
            throw new DomainException("Order must have at least one item");

        Status = OrderStatus.Confirmed;
        UpdatedAt = DateTime.UtcNow;
        RaiseDomainEvent(new OrderConfirmedEvent(Id));
    }
}
```

## Repository Interface (per aggregate root)
```csharp
public interface IOrderRepository
{
    Task<Order?> GetAsync(Guid id, CancellationToken ct = default);
    Task AddAsync(Order order, CancellationToken ct = default);
    Task UpdateAsync(Order order, CancellationToken ct = default);
}
```

## EF Core Configuration
```csharp
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.HasKey(o => o.Id);
        builder.OwnsMany(o => o.Items, items =>
        {
            items.ToTable("OrderItems");
            items.WithOwner().HasForeignKey("OrderId");
        });
        builder.Ignore(o => o.DomainEvents); // Not persisted
    }
}
```

## Anti-Patterns
- Never expose mutable collections: use `IReadOnlyList<>`, never `List<>`
- Never set state directly from outside: all mutations through aggregate methods
- Never cross aggregate boundaries in a single transaction
- Never put query logic in aggregates — use separate read models
