A distributed transaction (2PC) is not an option.
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 |