Saga pattern in .NET microservices: keeping order, stock and payment consistent (C#)

A distributed transaction (2PC) is not an option.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// ======================================================================
// Q: Order, stock and payment are in different services. How do you keep them
// consistent?
//
// A distributed transaction (2PC) is not an option. I use a Saga: a sequence of
// local transactions, where each step has a compensating action in case a later
// step fails.
// - Choreography: each service listens to events and publishes its own. Pros:
//   simple for 2-3 steps, no central point. Cons: the flow is "hidden" across
//   many services, with a risk of cyclic events.
// - Orchestration: an orchestrator (state machine) sends commands and keeps the
//   state. Pros: the flow is visible in one place, timeouts and compensations
//   are easy. Cons: the orchestrator can turn into a "god service".
// Tools: MassTransit state machines, NServiceBus sagas, Azure Durable Functions,
// Temporal. The user sees intermediate states (e.g. "Pending payment"): that is
// eventual consistency, and it must be visible in the UI too.
// ======================================================================

// Orchestration: the saga as an aggregate with state, persisted in the database
public class OrderFulfillmentSaga
{
    public Guid OrderId { get; private set; }
    public SagaState State { get; private set; } = SagaState.AwaitingStock;

    public IEnumerable<object> On(StockReserved e)
    {
        State = SagaState.AwaitingPayment;
        yield return new ChargePayment(OrderId, e.Amount);            // next command
    }

    public IEnumerable<object> On(PaymentFailed e)
    {
        State = SagaState.Compensating;
        yield return new ReleaseStock(OrderId);                       // compensation
        yield return new CancelOrder(OrderId, e.Reason);
    }
}
// Commands go out via the outbox (Q10), so state and messages are saved together