Files
pos-system/microservices/.agent/skills/saga-pattern/SKILL.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

21 KiB

name, description, compatibility, metadata
name description compatibility metadata
saga-pattern SAGA Pattern - quản lý distributed transactions qua chuỗi local transactions với compensating actions. Use for checkout flows, order processing, và multi-service orchestration. .NET 10+, MassTransit, RabbitMQ, EF Core
author version
Velik Ho 1.0

SAGA Pattern / Mẫu SAGA

Pattern quản lý distributed transactions trong microservices bằng chuỗi local transactions.

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

Use this skill when:

  • Multi-service transactions required / Cần transactions đa dịch vụ
  • Distributed transaction (2PC) not feasible / 2PC không khả thi
  • Long-running business processes / Quy trình nghiệp vụ chạy lâu
  • Need compensating transactions / Cần transactions bù trừ

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

Why SAGA? / Tại Sao Dùng SAGA?

┌─────────────────────────────────────────────────────────────┐
│     ❌ DISTRIBUTED TRANSACTION (2PC) - PROBLEMS             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌───────┐     ┌───────┐     ┌───────┐                    │
│   │Order  │────▶│Payment│────▶│Inventory│                   │
│   │ DB    │     │ DB    │     │ DB      │                   │
│   └───────┘     └───────┘     └───────┘                    │
│       │             │             │                         │
│       └─────────────┴─────────────┘                         │
│              Transaction Coordinator                        │
│                                                             │
│   Problems:                                                 │
│   • Locks resources across services                         │
│   • Single point of failure                                 │
│   • Poor performance                                        │
│   • Not always supported (NoSQL, external APIs)            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│              ✅ SAGA - LOCAL TRANSACTIONS                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Step 1          Step 2          Step 3                    │
│   ┌───────┐      ┌───────┐      ┌───────┐                  │
│   │Create │─────▶│Reserve│─────▶│Charge │                  │
│   │Order  │      │Stock  │      │Payment│                  │
│   └───────┘      └───────┘      └───────┘                  │
│       │              │              │                       │
│       ▼              ▼              ▼                       │
│   [Local Tx]    [Local Tx]    [Local Tx]                   │
│                                                             │
│   If Step 3 fails:                                         │
│   ┌───────┐      ┌───────┐                                 │
│   │Release│◀─────│Cancel │                                 │
│   │Stock  │      │Order  │                                 │
│   └───────┘      └───────┘                                 │
│   Compensate     Compensate                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Choreography vs Orchestration

┌─────────────────────────────────────────────────────────────┐
│                    CHOREOGRAPHY                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Order     Event Bus      Inventory      Payment           │
│     │           │              │             │              │
│     │──Create──▶│              │             │              │
│     │           │──OrderCreated─▶│           │              │
│     │           │              │──Reserve──▶│              │
│     │           │              │             │──Charge────▶ │
│     │           │◀─────PaymentProcessed──────│              │
│                                                             │
│   ✅ Loose coupling, simple services                        │
│   ❌ Hard to track flow, complex rollback                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                    ORCHESTRATION                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│                    ┌─────────────┐                          │
│                    │    SAGA     │                          │
│                    │ Orchestrator│                          │
│                    └─────┬───────┘                          │
│           ┌──────────────┼──────────────┐                   │
│           ▼              ▼              ▼                   │
│      ┌─────────┐   ┌─────────┐   ┌─────────┐               │
│      │ Order   │   │Inventory│   │ Payment │               │
│      │ Service │   │ Service │   │ Service │               │
│      └─────────┘   └─────────┘   └─────────┘               │
│                                                             │
│   ✅ Clear flow, centralized error handling                 │
│   ❌ More coupling, orchestrator complexity                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

SAGA Components / Thành Phần SAGA

Component Purpose Example
Saga State Machine Tracks saga state OrderSaga
Saga Step Individual transaction ReserveInventory
Compensating Action Undo step on failure ReleaseInventory
Saga Data Shared state across steps OrderId, Items

Key Patterns / Mẫu Chính

Saga State Machine with MassTransit

/// <summary>
/// EN: Order saga state tracking.
/// VI: Theo dõi state của Order saga.
/// </summary>
public class OrderSagaState : SagaStateMachineInstance
{
    public Guid CorrelationId { get; set; }
    public string CurrentState { get; set; } = default!;
    
    // EN: Saga data / VI: Dữ liệu saga
    public Guid OrderId { get; set; }
    public string UserId { get; set; } = default!;
    public decimal TotalAmount { get; set; }
    public string? PaymentId { get; set; }
    public string? FailureReason { get; set; }
}

/// <summary>
/// EN: Order saga state machine definition.
/// VI: Định nghĩa state machine cho Order saga.
/// </summary>
public class OrderSaga : MassTransitStateMachine<OrderSagaState>
{
    // EN: States / VI: Các trạng thái
    public State OrderCreated { get; private set; } = null!;
    public State InventoryReserved { get; private set; } = null!;
    public State PaymentProcessed { get; private set; } = null!;
    public State Completed { get; private set; } = null!;
    public State Failed { get; private set; } = null!;

    // EN: Events / VI: Các sự kiện
    public Event<OrderSubmitted> OrderSubmittedEvent { get; private set; } = null!;
    public Event<InventoryReserved> InventoryReservedEvent { get; private set; } = null!;
    public Event<InventoryReservationFailed> InventoryFailedEvent { get; private set; } = null!;
    public Event<PaymentProcessed> PaymentProcessedEvent { get; private set; } = null!;
    public Event<PaymentFailed> PaymentFailedEvent { get; private set; } = null!;

    public OrderSaga()
    {
        InstanceState(x => x.CurrentState);

        // EN: Define events / VI: Định nghĩa events
        Event(() => OrderSubmittedEvent, x => x.CorrelateById(m => m.Message.OrderId));
        Event(() => InventoryReservedEvent, x => x.CorrelateById(m => m.Message.OrderId));
        Event(() => InventoryFailedEvent, x => x.CorrelateById(m => m.Message.OrderId));
        Event(() => PaymentProcessedEvent, x => x.CorrelateById(m => m.Message.OrderId));
        Event(() => PaymentFailedEvent, x => x.CorrelateById(m => m.Message.OrderId));

        // EN: Define state transitions / VI: Định nghĩa chuyển trạng thái
        Initially(
            When(OrderSubmittedEvent)
                .Then(context =>
                {
                    context.Saga.OrderId = context.Message.OrderId;
                    context.Saga.UserId = context.Message.UserId;
                    context.Saga.TotalAmount = context.Message.TotalAmount;
                })
                .Publish(context => new ReserveInventory
                {
                    OrderId = context.Saga.OrderId,
                    Items = context.Message.Items
                })
                .TransitionTo(OrderCreated)
        );

        During(OrderCreated,
            When(InventoryReservedEvent)
                .Publish(context => new ProcessPayment
                {
                    OrderId = context.Saga.OrderId,
                    UserId = context.Saga.UserId,
                    Amount = context.Saga.TotalAmount
                })
                .TransitionTo(InventoryReserved),
                
            When(InventoryFailedEvent)
                .Then(context => context.Saga.FailureReason = context.Message.Reason)
                .Publish(context => new CancelOrder
                {
                    OrderId = context.Saga.OrderId,
                    Reason = context.Message.Reason
                })
                .TransitionTo(Failed)
        );

        During(InventoryReserved,
            When(PaymentProcessedEvent)
                .Then(context => context.Saga.PaymentId = context.Message.PaymentId)
                .Publish(context => new CompleteOrder
                {
                    OrderId = context.Saga.OrderId,
                    PaymentId = context.Message.PaymentId
                })
                .TransitionTo(Completed)
                .Finalize(),
                
            When(PaymentFailedEvent)
                .Then(context => context.Saga.FailureReason = context.Message.Reason)
                // EN: Compensating action - release inventory
                // VI: Action bù trừ - giải phóng inventory
                .Publish(context => new ReleaseInventory
                {
                    OrderId = context.Saga.OrderId
                })
                .Publish(context => new CancelOrder
                {
                    OrderId = context.Saga.OrderId,
                    Reason = context.Message.Reason
                })
                .TransitionTo(Failed)
        );

        SetCompletedWhenFinalized();
    }
}

Saga Events / Events của SAGA

/// <summary>
/// EN: Events for order saga.
/// VI: Events cho order saga.
/// </summary>

// EN: Trigger events / VI: Events kích hoạt
public record OrderSubmitted
{
    public Guid OrderId { get; init; }
    public string UserId { get; init; } = default!;
    public decimal TotalAmount { get; init; }
    public List<OrderItemInfo> Items { get; init; } = new();
}

// EN: Command messages / VI: Command messages
public record ReserveInventory
{
    public Guid OrderId { get; init; }
    public List<OrderItemInfo> Items { get; init; } = new();
}

public record ProcessPayment
{
    public Guid OrderId { get; init; }
    public string UserId { get; init; } = default!;
    public decimal Amount { get; init; }
}

public record ReleaseInventory  // Compensating
{
    public Guid OrderId { get; init; }
}

public record CancelOrder  // Compensating
{
    public Guid OrderId { get; init; }
    public string Reason { get; init; } = default!;
}

public record CompleteOrder
{
    public Guid OrderId { get; init; }
    public string PaymentId { get; init; } = default!;
}

// EN: Response events / VI: Events phản hồi
public record InventoryReserved
{
    public Guid OrderId { get; init; }
}

public record InventoryReservationFailed
{
    public Guid OrderId { get; init; }
    public string Reason { get; init; } = default!;
}

public record PaymentProcessed
{
    public Guid OrderId { get; init; }
    public string PaymentId { get; init; } = default!;
}

public record PaymentFailed
{
    public Guid OrderId { get; init; }
    public string Reason { get; init; } = default!;
}

Saga Consumer / Consumer của SAGA

/// <summary>
/// EN: Inventory service consumer for saga.
/// VI: Consumer của Inventory service cho saga.
/// </summary>
public class ReserveInventoryConsumer : IConsumer<ReserveInventory>
{
    private readonly IInventoryRepository _repository;
    private readonly ILogger<ReserveInventoryConsumer> _logger;

    public ReserveInventoryConsumer(
        IInventoryRepository repository,
        ILogger<ReserveInventoryConsumer> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public async Task Consume(ConsumeContext<ReserveInventory> context)
    {
        var message = context.Message;
        
        try
        {
            foreach (var item in message.Items)
            {
                var success = await _repository.ReserveAsync(
                    item.ProductId, 
                    item.Quantity,
                    message.OrderId,
                    context.CancellationToken);

                if (!success)
                {
                    // EN: Not enough stock, publish failure
                    // VI: Không đủ hàng, publish failure
                    await context.Publish(new InventoryReservationFailed
                    {
                        OrderId = message.OrderId,
                        Reason = $"Insufficient stock for product {item.ProductId}"
                    });
                    return;
                }
            }

            // EN: All items reserved successfully
            // VI: Tất cả items đã được reserve thành công
            await context.Publish(new InventoryReserved
            {
                OrderId = message.OrderId
            });

            _logger.LogInformation("Inventory reserved for order {OrderId}", message.OrderId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to reserve inventory for order {OrderId}", message.OrderId);
            
            await context.Publish(new InventoryReservationFailed
            {
                OrderId = message.OrderId,
                Reason = ex.Message
            });
        }
    }
}

/// <summary>
/// EN: Compensating consumer to release inventory.
/// VI: Consumer bù trừ để giải phóng inventory.
/// </summary>
public class ReleaseInventoryConsumer : IConsumer<ReleaseInventory>
{
    private readonly IInventoryRepository _repository;
    private readonly ILogger<ReleaseInventoryConsumer> _logger;

    public ReleaseInventoryConsumer(
        IInventoryRepository repository,
        ILogger<ReleaseInventoryConsumer> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public async Task Consume(ConsumeContext<ReleaseInventory> context)
    {
        await _repository.ReleaseReservationAsync(
            context.Message.OrderId,
            context.CancellationToken);

        _logger.LogInformation(
            "Released inventory reservation for order {OrderId}",
            context.Message.OrderId);
    }
}

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

1. Missing Compensating Actions

// ❌ BAD: No compensation handling
During(InventoryReserved,
    When(PaymentFailedEvent)
        .TransitionTo(Failed)  // Inventory still reserved!
);

// ✅ GOOD: Proper compensation
During(InventoryReserved,
    When(PaymentFailedEvent)
        .Publish(context => new ReleaseInventory
        {
            OrderId = context.Saga.OrderId
        })
        .TransitionTo(Failed)
);

2. Non-Idempotent Compensations

// ❌ BAD: Can fail if called twice
public async Task Consume(ConsumeContext<ReleaseInventory> context)
{
    var reservation = await _repo.GetReservationAsync(context.Message.OrderId);
    await _repo.DeleteAsync(reservation);  // Throws if already deleted
}

// ✅ GOOD: Idempotent compensation
public async Task Consume(ConsumeContext<ReleaseInventory> context)
{
    var reservation = await _repo.GetReservationAsync(context.Message.OrderId);
    if (reservation != null)
    {
        await _repo.DeleteAsync(reservation);
    }
    // Safe to call multiple times
}

3. No Timeout Handling

// ❌ BAD: Saga can hang forever
public OrderSaga()
{
    During(OrderCreated,
        When(InventoryReservedEvent)
            .TransitionTo(InventoryReserved)
    );
    // What if InventoryReservedEvent never arrives?
}

// ✅ GOOD: Add timeout handling
public OrderSaga()
{
    Schedule(() => ReservationTimeout, x => x.ReservationTimeoutToken, 
        s => s.Delay = TimeSpan.FromMinutes(5));

    During(OrderCreated,
        When(InventoryReservedEvent)
            .Unschedule(ReservationTimeout)
            .TransitionTo(InventoryReserved),
            
        When(ReservationTimeout.Received)
            .Then(ctx => ctx.Saga.FailureReason = "Reservation timeout")
            .Publish(ctx => new CancelOrder { OrderId = ctx.Saga.OrderId })
            .TransitionTo(Failed)
    );
}

Quick Reference / Tham Chiếu Nhanh

Choreography vs Orchestration

Aspect Choreography Orchestration
Coupling Low Higher
Complexity Distributed Centralized
Debugging Hard Easier
Single point of failure No Yes (orchestrator)
Best for Simple flows Complex flows

SAGA Design Checklist

Step Required
Define all saga steps
Define compensating action for each step
Make compensations idempotent
Add timeout handling
Log state transitions
Design for partial failures

Resources / Tài Nguyên