The payment succeeded. The shipping booking failed. What should the order page say now?
That uncomfortable gap is where a multi-service workflow becomes more than a sequence of API calls. Each service may have saved its own result, and a database rollback in the order service won't undo a payment made elsewhere.
A saga gives us a way to manage the steps and their recovery. Let's follow an order through a successful payment and a failed booking, then work out what it takes to reach an honest final state.
Start with transactions each service can own
Suppose our shop has an order service, a payment service and a shipping service. Each owns its data and commits its own local transactions.
Creating the order may succeed before payment begins. Payment may succeed before shipping is booked. The saga connects those separate outcomes into one business process without pretending they committed as a single database transaction.
Distributed transactions are another possible design in suitable systems; they don't cease to exist across service boundaries. A saga chooses a different approach, with intermediate states and recovery actions we must explicitly design.
Give the workflow somewhere to remember its progress
In an orchestrated saga, a coordinator decides the next step and records progress. For our example, it could move through these states:
- Create a pending order.
- Request payment with a stable operation ID.
- Record the confirmed payment outcome.
- Request a shipping booking.
- Confirm the order after the required steps succeed.

This progress must survive a restart. If the coordinator forgets that payment succeeded, it could request another payment after recovering. Persist the workflow state and use operation IDs that remain the same across retries.
Commands ask a service to act. Events report what happened. A BookShipping command and a ShippingBooked event have different meanings, even when both travel through the same broker.
A failed booking needs a new action
Now shipping returns a definite failure: no available service for the address. Our policy says an order without shipping cannot proceed.
The saga asks for a refund and cancellation. Those are compensating actions: new operations intended to recover from earlier completed work. They don't rewind time or remove the original payment from history.
A refund may take time, carry provider rules or fail temporarily. The order therefore needs a state such as refund_pending, with a path to cancelled only when the required recovery is confirmed. Telling the customer "refunded" when we've merely requested it is a separate bug.
This is why compensation is a business decision, not simply the reverse of every function call. The recovery for a reserved item, captured payment and dispatched parcel can be different.
Whatever it takes. Refund included.
A timeout is an unknown outcome
A failed shipping response is one thing. No response is another.
If the courier timed out after booking the shipment, refunding and cancelling immediately could leave a parcel moving towards a cancelled order. First resolve the uncertain outcome through a status lookup, repeat-safe operation or other supported reconciliation process.
Are we reacting to a confirmed failure, or to a result we haven't managed to learn yet?
Keep those states distinct in the workflow. Use deadlines and escalation rules, but don't silently convert "unknown" into "didn't happen." A saga must handle uncertainty as well as rejection.
Recovery can need recovery too
Suppose the refund request fails because the provider is temporarily unavailable. We haven't reached the final cancelled state; we have unfinished compensating work.
Record the pending action, retry under a bounded policy, and make it visible to an operator when automatic recovery cannot finish. That operator needs the order ID, payment reference, last outcome and next safe action.
A state diagram makes the happy and unhappy paths easier to discuss:
This simplified diagram shows known outcomes. Timeouts need a separate reconciliation state before we choose either branch.
Coordination can also happen through events
An alternative is choreography: services respond to events and trigger subsequent steps without one central coordinator deciding every move. For example, a payment event may cause shipping to begin.
That can work well for a small, understandable flow. As the process grows, tracing who reacts to which event and who owns recovery can become harder. Orchestration makes the workflow explicit, but adds a coordinator to maintain. The saga pattern guidance compares the approaches.
Neither approach gives us automatic isolation from concurrent work. Another request may observe intermediate states, and repeated messages still need safe handling. Stock reservations, payment references and order transitions must enforce the rules the shop actually needs.
Write the failure path before calling it finished
For this order, the success path is easy to list. The useful design work is deciding what happens after payment, what an uncertain booking means, and how a failed refund becomes visible.
A saga makes those decisions part of the application instead of leaving them to an incident. Before implementing one, walk through each step and ask what is already committed, what can be compensated, and what requires human attention. The final state should mean what the customer thinks it means.
Try it yourself
Payment succeeded, shipping timed out, and the saga has no confirmed booking outcome. Should it immediately refund and mark the order cancelled?
Share this article with your friends
Know someone who would enjoy this? Send it their way.