A practical lesson on choreographed workflows, including where distributed reaction models work well and where they become hard to trace and govern.
Choreography coordinates a business process by letting services react to events from one another rather than by having one central component issue commands step by step. One service finishes local work and emits an event. Another service listens, does its own work, and emits the next event. The workflow emerges from these reactions.
This style can be attractive because it preserves service ownership and avoids one obvious central coordinator. Each service handles its own local rule: reserve inventory, authorize payment, create shipment, send confirmation. The architecture feels naturally event-driven because the process advances through published business facts.
The risk is that the process can become invisible. Once the workflow is spread across many services and topics, the system may still function, but the business path becomes harder to see, test, and reason about during failure.
flowchart LR
A["order.placed"] --> B["Inventory reserves stock"]
B --> C["inventory.reserved"]
C --> D["Payment authorizes card"]
D --> E["payment.authorized"]
E --> F["Shipping creates shipment"]
What to notice:
Choreography works well when:
This often fits simpler domain progressions such as user onboarding steps, notification pipelines, and moderate business flows where each service can continue from a clear prior fact.
The pattern is strongest when each event still represents a meaningful domain fact. If services start publishing “please do step 2 now” style events that are really hidden commands, the architecture may claim choreography while quietly losing its business clarity.
The more services participate, the harder it becomes to answer questions like:
This is why choreography often starts elegant and later feels opaque. The local service code still looks reasonable, but the overall process has no one obvious place to inspect.
1type InventoryReserved = {
2 eventName: "inventory.reserved";
3 data: { orderId: string; reservationId: string };
4};
5
6async function handleInventoryReserved(event: InventoryReserved) {
7 await paymentService.authorize({
8 orderId: event.data.orderId,
9 reservationId: event.data.reservationId,
10 });
11
12 await eventBus.publish({
13 eventName: "payment.authorization.requested",
14 data: { orderId: event.data.orderId },
15 });
16}
This code is simple locally. The architectural question is whether the end-to-end process is still visible when many similar handlers exist across the platform.
One common mistake is to think choreography removes coordination. It does not. It distributes coordination. The workflow still exists. Timing, ordering, and compensation still matter. The system has simply chosen to let several services own those decisions through reactions instead of using one explicit controller.
That means choreography needs:
Without those controls, the process becomes hidden rather than decoupled.
A team says choreography is always better because there is no central workflow engine to maintain. What is the stronger challenge?
The stronger challenge is that removing a central controller does not remove workflow complexity. It moves that complexity into event chains, service reactions, and distributed failure handling. If the process is critical or difficult to trace, the cost of hidden coordination may exceed the benefit of avoiding a central coordinator.