# Integration Standard: Event Sourcing

## Overview
State is derived from a sequence of immutable events. Use only when audit trail or temporal queries are required.

## When to Use
✅ Financial transactions (audit required by law)
✅ Order lifecycle (multiple state transitions, replay needed)
✅ Inventory changes (why did stock go negative?)
❌ User profiles (too much churn, GDPR deletion complexity)
❌ Session/cache state (ephemeral, no audit need)

## Event Design
```csharp
// Base event
public abstract record DomainEvent
{
    public Guid EventId { get; init; } = Guid.NewGuid();
    public DateTime OccurredAt { get; init; } = DateTime.UtcNow;
    public int Version { get; init; }
    public string EventType => GetType().Name;
}

// Specific events — past tense, immutable
public record OrderPlaced(Guid OrderId, Guid UserId, decimal Total, List<OrderItem> Items)
    : DomainEvent;

public record OrderShipped(Guid OrderId, string TrackingNumber, DateTime ShippedAt)
    : DomainEvent;

public record OrderCancelled(Guid OrderId, string Reason)
    : DomainEvent;
```

## Aggregate with Event Sourcing
```csharp
public class Order : AggregateRoot
{
    public OrderStatus Status { get; private set; }
    public decimal Total { get; private set; }

    // Reconstruct from events
    public static Order Replay(IEnumerable<DomainEvent> events)
    {
        var order = new Order();
        foreach (var e in events) order.Apply(e);
        return order;
    }

    // Command → validate → raise event
    public void Ship(string trackingNumber)
    {
        if (Status != OrderStatus.Confirmed)
            throw new InvalidOperationException($"Cannot ship order in {Status} status");

        RaiseEvent(new OrderShipped(Id, trackingNumber, DateTime.UtcNow));
    }

    // State transition from event (idempotent)
    protected override void Apply(DomainEvent @event)
    {
        switch (@event)
        {
            case OrderPlaced e:
                Id = e.OrderId;
                Total = e.Total;
                Status = OrderStatus.Pending;
                break;

            case OrderShipped e:
                Status = OrderStatus.Shipped;
                break;

            case OrderCancelled:
                Status = OrderStatus.Cancelled;
                break;
        }
    }
}
```

## Event Store (Azure Cosmos DB pattern)
```csharp
public class CosmosEventStore : IEventStore
{
    public async Task AppendAsync(Guid streamId, IEnumerable<DomainEvent> events, int expectedVersion)
    {
        foreach (var (evt, i) in events.Select((e, i) => (e, i)))
        {
            var document = new EventDocument
            {
                Id = $"{streamId}:{expectedVersion + i + 1}",
                StreamId = streamId.ToString(),
                Version = expectedVersion + i + 1,
                EventType = evt.EventType,
                Data = JsonSerializer.Serialize(evt, evt.GetType()),
                OccurredAt = evt.OccurredAt
            };
            await _container.CreateItemAsync(document, new PartitionKey(streamId.ToString()));
        }
    }
}
```

## Projections (Read Models)
```csharp
// Project events to read-optimized views
public class OrderSummaryProjection
{
    public void Handle(OrderPlaced e, AppDbContext ctx)
        => ctx.OrderSummaries.Add(new OrderSummary { Id = e.OrderId, Total = e.Total, Status = "Pending" });

    public void Handle(OrderShipped e, AppDbContext ctx)
        => ctx.OrderSummaries.Where(s => s.Id == e.OrderId).ExecuteUpdate(s => s.SetProperty(p => p.Status, "Shipped"));
}
```

## Trade-offs
| Pro | Con |
|-----|-----|
| Complete audit trail | Schema evolution complexity |
| Temporal queries (state at time T) | Higher storage cost |
| Event replay for debugging | Eventual consistency for read models |
| Natural integration events | GDPR deletion requires event masking |
