---
name: event-sourcing
description: Event Sourcing pattern - lưu trữ thay đổi trạng thái dưới dạng chuỗi sự kiện. Use for audit trails, temporal queries, CQRS integration, và event replay.
compatibility: ".NET 10+, EventStoreDB, Marten, EF Core"
metadata:
  author: Velik Ho
  version: "1.0"
---

# Event Sourcing / Event Sourcing Pattern

Event Sourcing pattern cho GoodGo microservices - lưu trữ mọi thay đổi trạng thái dưới dạng sự kiện bất biến.

## When to Use This Skill / Khi Nào Sử Dụng

Use this skill when:
- Building audit trails / Xây dựng audit trails
- Implementing temporal queries (time travel) / Truy vấn theo thời gian
- Debugging production issues / Debug lỗi production
- CQRS with event-driven projections / CQRS với projections
- Ensuring data consistency / Đảm bảo tính nhất quán dữ liệu

## Core Concepts / Khái Niệm Cốt Lõi

### Traditional vs Event Sourcing / Truyền Thống vs Event Sourcing

```
┌─────────────────────────────────────────────────────────────┐
│                   TRADITIONAL (State-Based)                  │
├─────────────────────────────────────────────────────────────┤
│  Order Table                                                 │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ Id: 1, Status: Shipped, Amount: 500, Updated: 10:30 │    │
│  └─────────────────────────────────────────────────────┘    │
│  ❌ Lost: Why was it shipped? What was previous status?    │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                   EVENT SOURCING (Event-Based)               │
├─────────────────────────────────────────────────────────────┤
│  Event Stream: Order-1                                       │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ 1. OrderCreated { Amount: 500, UserId: "u1" } @10:00│    │
│  │ 2. OrderPaid { PaymentId: "p1" } @10:15             │    │
│  │ 3. OrderShipped { TrackingNo: "TN123" } @10:30      │    │
│  └─────────────────────────────────────────────────────┘    │
│  ✅ Full history preserved                                  │
└─────────────────────────────────────────────────────────────┘
```

### Key Components / Thành Phần Chính

| Component | Purpose | Description |
|-----------|---------|-------------|
| **Event** | Immutable fact | Sự kiện đã xảy ra, không thể thay đổi |
| **Event Stream** | Ordered sequence | Chuỗi events theo thời gian cho mỗi aggregate |
| **Event Store** | Append-only log | Database lưu trữ events |
| **Projection** | Read model | View được xây dựng từ events |
| **Snapshot** | State cache | Cache trạng thái để tối ưu replay |

### Event Sourcing Flow / Luồng Event Sourcing

```
┌─────────┐    ┌─────────────┐    ┌─────────────┐
│ Command │───▶│  Aggregate  │───▶│   Events    │
└─────────┘    │  (Domain)   │    │  (Facts)    │
               └─────────────┘    └──────┬──────┘
                                         │
               ┌─────────────────────────┼─────────────────────────┐
               │                         │                         │
               ▼                         ▼                         ▼
        ┌─────────────┐          ┌─────────────┐          ┌─────────────┐
        │ Event Store │          │  Projection │          │  Snapshot   │
        │ (Write)     │          │  (Read)     │          │  (Cache)    │
        └─────────────┘          └─────────────┘          └─────────────┘
```

## Key Patterns / Mẫu Chính

### Domain Event Definition / Định Nghĩa Domain Event

```csharp
/// <summary>
/// EN: Base interface for all domain events.
/// VI: Interface cơ sở cho tất cả domain events.
/// </summary>
public interface IDomainEvent
{
    Guid EventId { get; }
    DateTime OccurredOn { get; }
    int Version { get; }
}

/// <summary>
/// EN: Order created event.
/// VI: Event tạo order.
/// </summary>
public record OrderCreated : IDomainEvent
{
    public Guid EventId { get; init; } = Guid.NewGuid();
    public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
    public int Version { get; init; }
    
    public Guid OrderId { get; init; }
    public string UserId { get; init; } = default!;
    public decimal TotalAmount { get; init; }
    public Address ShippingAddress { get; init; } = default!;
}

/// <summary>
/// EN: Order item added event.
/// VI: Event thêm item vào order.
/// </summary>
public record OrderItemAdded : IDomainEvent
{
    public Guid EventId { get; init; } = Guid.NewGuid();
    public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
    public int Version { get; init; }
    
    public Guid OrderId { get; init; }
    public Guid ProductId { get; init; }
    public int Quantity { get; init; }
    public decimal UnitPrice { get; init; }
}
```

### Event-Sourced Aggregate / Aggregate Event-Sourced

```csharp
/// <summary>
/// EN: Base class for event-sourced aggregates.
/// VI: Lớp cơ sở cho aggregate event-sourced.
/// </summary>
public abstract class EventSourcedAggregate
{
    private readonly List<IDomainEvent> _uncommittedEvents = new();
    
    public Guid Id { get; protected set; }
    public int Version { get; protected set; } = -1;
    
    public IReadOnlyList<IDomainEvent> UncommittedEvents => _uncommittedEvents.AsReadOnly();
    
    protected void Apply(IDomainEvent @event)
    {
        When(@event);
        _uncommittedEvents.Add(@event);
        Version++;
    }
    
    protected abstract void When(IDomainEvent @event);
    
    public void Load(IEnumerable<IDomainEvent> history)
    {
        foreach (var @event in history)
        {
            When(@event);
            Version++;
        }
    }
    
    public void ClearUncommittedEvents() => _uncommittedEvents.Clear();
}

/// <summary>
/// EN: Order aggregate with event sourcing.
/// VI: Order aggregate với event sourcing.
/// </summary>
public class Order : EventSourcedAggregate
{
    public string UserId { get; private set; } = default!;
    public OrderStatus Status { get; private set; }
    public decimal TotalAmount { get; private set; }
    private readonly List<OrderItem> _items = new();
    public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();

    // EN: For rehydration / VI: Để khôi phục từ events
    private Order() { }

    // EN: Factory method for creation / VI: Factory method để tạo mới
    public static Order Create(Guid orderId, string userId, Address shippingAddress)
    {
        var order = new Order();
        order.Apply(new OrderCreated
        {
            OrderId = orderId,
            UserId = userId,
            ShippingAddress = shippingAddress,
            Version = 0
        });
        return order;
    }

    public void AddItem(Guid productId, int quantity, decimal unitPrice)
    {
        Apply(new OrderItemAdded
        {
            OrderId = Id,
            ProductId = productId,
            Quantity = quantity,
            UnitPrice = unitPrice,
            Version = Version + 1
        });
    }

    protected override void When(IDomainEvent @event)
    {
        switch (@event)
        {
            case OrderCreated e:
                Id = e.OrderId;
                UserId = e.UserId;
                Status = OrderStatus.Created;
                break;
                
            case OrderItemAdded e:
                _items.Add(new OrderItem(e.ProductId, e.Quantity, e.UnitPrice));
                TotalAmount += e.Quantity * e.UnitPrice;
                break;
                
            case OrderPaid:
                Status = OrderStatus.Paid;
                break;
        }
    }
}
```

### Event Store Repository / Repository Event Store

```csharp
/// <summary>
/// EN: Repository for event-sourced aggregates.
/// VI: Repository cho aggregate event-sourced.
/// </summary>
public interface IEventSourcedRepository<T> where T : EventSourcedAggregate
{
    Task<T?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task SaveAsync(T aggregate, CancellationToken ct = default);
}

/// <summary>
/// EN: EF Core implementation of event store.
/// VI: Event store triển khai với EF Core.
/// </summary>
public class EfCoreEventStore<T> : IEventSourcedRepository<T> 
    where T : EventSourcedAggregate, new()
{
    private readonly EventStoreDbContext _context;
    private readonly ILogger<EfCoreEventStore<T>> _logger;

    public EfCoreEventStore(
        EventStoreDbContext context,
        ILogger<EfCoreEventStore<T>> logger)
    {
        _context = context;
        _logger = logger;
    }

    public async Task<T?> GetByIdAsync(Guid id, CancellationToken ct = default)
    {
        var streamName = $"{typeof(T).Name}-{id}";
        
        var events = await _context.Events
            .Where(e => e.StreamId == streamName)
            .OrderBy(e => e.Version)
            .ToListAsync(ct);

        if (!events.Any())
            return null;

        var aggregate = new T();
        aggregate.Load(events.Select(e => DeserializeEvent(e)));
        
        return aggregate;
    }

    public async Task SaveAsync(T aggregate, CancellationToken ct = default)
    {
        var streamName = $"{typeof(T).Name}-{aggregate.Id}";
        var expectedVersion = aggregate.Version - aggregate.UncommittedEvents.Count;

        // EN: Optimistic concurrency check
        // VI: Kiểm tra optimistic concurrency
        var currentVersion = await _context.Events
            .Where(e => e.StreamId == streamName)
            .MaxAsync(e => (int?)e.Version, ct) ?? -1;

        if (currentVersion != expectedVersion)
            throw new ConcurrencyException(
                $"Expected version {expectedVersion} but found {currentVersion}");

        foreach (var @event in aggregate.UncommittedEvents)
        {
            _context.Events.Add(new StoredEvent
            {
                Id = @event.EventId,
                StreamId = streamName,
                EventType = @event.GetType().AssemblyQualifiedName!,
                Data = JsonSerializer.Serialize(@event, @event.GetType()),
                Version = @event.Version,
                OccurredOn = @event.OccurredOn
            });
        }

        await _context.SaveChangesAsync(ct);
        aggregate.ClearUncommittedEvents();
        
        _logger.LogInformation(
            "Saved {Count} events to stream {Stream}",
            aggregate.UncommittedEvents.Count,
            streamName);
    }
}
```

### Projections / Projections

```csharp
/// <summary>
/// EN: Projection handler for order read models.
/// VI: Projection handler cho order read models.
/// </summary>
public class OrderProjection : IEventHandler<OrderCreated>, IEventHandler<OrderPaid>
{
    private readonly ReadDbContext _readDb;

    public OrderProjection(ReadDbContext readDb)
    {
        _readDb = readDb;
    }

    public async Task HandleAsync(OrderCreated @event, CancellationToken ct)
    {
        var readModel = new OrderReadModel
        {
            Id = @event.OrderId,
            UserId = @event.UserId,
            Status = "Created",
            TotalAmount = @event.TotalAmount,
            CreatedAt = @event.OccurredOn
        };

        _readDb.Orders.Add(readModel);
        await _readDb.SaveChangesAsync(ct);
    }

    public async Task HandleAsync(OrderPaid @event, CancellationToken ct)
    {
        var order = await _readDb.Orders.FindAsync(@event.OrderId);
        if (order != null)
        {
            order.Status = "Paid";
            order.PaidAt = @event.OccurredOn;
            await _readDb.SaveChangesAsync(ct);
        }
    }
}
```

## Common Mistakes / Lỗi Thường Gặp

### 1. Mutable Events

```csharp
// ❌ BAD: Mutable event
public class OrderCreated
{
    public string Status { get; set; } // Mutable!
}

// ✅ GOOD: Immutable record
public record OrderCreated
{
    public string Status { get; init; } // Immutable
}
```

### 2. Storing Derived Data in Events

```csharp
// ❌ BAD: Storing computed values
public record OrderItemAdded
{
    public decimal UnitPrice { get; init; }
    public int Quantity { get; init; }
    public decimal TotalPrice { get; init; } // Derived!
}

// ✅ GOOD: Store only facts, compute when needed
public record OrderItemAdded
{
    public decimal UnitPrice { get; init; }
    public int Quantity { get; init; }
    // TotalPrice computed in aggregate
}
```

### 3. Large Event Streams Without Snapshots

```csharp
// ❌ BAD: Loading thousands of events
var order = await _repo.GetByIdAsync(orderId); // Slow!

// ✅ GOOD: Use snapshots for performance
public async Task<Order?> GetByIdAsync(Guid id, CancellationToken ct)
{
    // Load from snapshot if exists
    var snapshot = await _snapshotStore.GetLatestAsync<Order>(id, ct);
    var fromVersion = snapshot?.Version ?? 0;
    
    // Load only events after snapshot
    var events = await _eventStore.GetEventsAsync(id, fromVersion, ct);
    
    var aggregate = snapshot ?? new Order();
    aggregate.Load(events);
    return aggregate;
}
```

## Quick Reference / Tham Chiếu Nhanh

### When to Use Event Sourcing

| Scenario | Recommendation |
|----------|---------------|
| Simple CRUD | ❌ Overkill |
| Complex domains | ✅ Use ES |
| Audit requirements | ✅ Use ES |
| Temporal queries | ✅ Use ES |
| High write volume | ⚠️ Consider carefully |

### Event Naming Conventions

| Type | Convention | Example |
|------|-----------|---------|
| Create | `{Entity}Created` | `OrderCreated` |
| Update | `{Entity}{Property}Changed` | `OrderStatusChanged` |
| Delete | `{Entity}Deleted` | `OrderDeleted` |
| Action | `{Entity}{Action}` | `OrderShipped` |

### Snapshot Strategy

| Trigger | When to Snapshot |
|---------|-----------------|
| Version-based | Every N events (e.g., 100) |
| Time-based | Every N minutes |
| Size-based | When aggregate size > threshold |

## Resources / Tài Nguyên

- [Detailed Examples](./references/REFERENCE.md) - Full code examples
- [CQRS MediatR](../cqrs-mediatr/SKILL.md) - CQRS patterns
- [Outbox Pattern](../outbox-pattern/SKILL.md) - Reliable event publishing
- [Repository Pattern](../repository-pattern/SKILL.md) - Data access patterns
- [Domain Driven Design](../domain-driven-design/SKILL.md) - DDD patterns
