On Day 28 of building OrderHub I wired four microservices into a distributed transaction using choreography: no coordinator, just services reacting to each other's events. It worked, but the order flow was emergent — no single component owned it, so understanding or changing the flow meant reading every service's listeners. Day 29 is the mirror image: an orchestration saga, where one central coordinator holds the entire flow as one explicit state machine. Here's how I built it in Spring Boot with Kafka, and the trade-off it makes.
The coordinator owns the flow as a state machine
A single SagaOrchestrator in order-service hears OrderPlaced and drives the order forward by sending commands and awaiting replies, advancing through an explicit enum of steps:
STARTED → AWAITING_STOCK → AWAITING_PAYMENT → AWAITING_SHIPMENT → COMPLETED
↘ CANCELLED
It issues ReserveStock, waits for StockReserved, then ProcessPayment, waits for PaymentApproved, then ScheduleShipment. One command per step. The flow never leaves this one place — you can point at a single component and read the whole business process, which is exactly what choreography couldn't give you.
Commands go out, replies come in
The coordinator keeps one OrchestrationState per order, keyed and guarded, and every reply feeds into a single onReply handler that decides the next command. Commands are imperative and addressed to a specific participant, on a saga-commands topic; replies come back on saga-replies.
void onReply(SagaReply reply) {
OrchestrationState state = states.get(reply.orderId());
switch (state.step()) {
case AWAITING_STOCK -> { state.step(AWAITING_PAYMENT); publish(new ProcessPayment(...)); }
case AWAITING_PAYMENT -> { state.step(AWAITING_SHIPMENT); publish(new ScheduleShipment(...)); }
case AWAITING_SHIPMENT-> { state.step(COMPLETED); }
}
}
Participants stop deciding the flow
This is the big shift from Day 28. The inventory, payment and shipping services no longer react to each other's facts and decide what happens next. Each becomes a thin command handler that runs only the kind of command it owns and replies — StockCommandHandler handles RESERVE_STOCK and RELEASE_STOCK, PaymentCommandHandler handles PROCESS_PAYMENT, and so on. They obey and answer; the coordinator does the thinking.
Compensation is an explicit reverse command
The failure discipline is the same as a choreographed saga — undo committed work with a compensating action — but here it's explicit rather than emergent. If payment is declined or stock is insufficient, the orchestrator issues the compensating ReleaseStock command in reverse, then moves the saga to CANCELLED. Because the coordinator owns the state, it knows precisely what has been done and therefore what must be undone.
Idempotency: the current step is the guard
Kafka is at-least-once, so a reply can be redelivered. The step guard handles it for free: a reply only advances the saga when it matches the step the orchestrator is currently waiting on. A redelivered reply for a saga that's already terminal simply gets dropped — no double-charge, no double-ship. Each participant command handler is also idempotent on the commandId.
Coexisting with choreography
The whole orchestration path is gated behind *.orchestration.enabled (default false), so it lives beside the Day-28 choreography without colliding — the same order flow, two coordination styles, switchable. One @EmbeddedKafka test proves both the happy path to COMPLETED and the payment-declined compensation to CANCELLED, with no Docker and no external broker.
The trade-off
Choreography gave zero coupling at the cost of an emergent, scattered flow. Orchestration makes the opposite trade: the flow lives in one readable state machine you can point at — at the cost of the coordinator being a component every participant now depends on. Neither is "correct"; you pick based on whether you value decoupling or a single owner of the process more. Next up, Day 30's transactional outbox closes the one gap both saga styles still share: a terminal fact can be lost if the broker blips at exactly the wrong moment.
Run the orchestrated saga and watch the coordinator drive it:
https://dev48v.infy.uk/orderhub.php
Full Spring Boot + React source:
https://github.com/dev48v/order-hub-from-zero
Top comments (0)