Run a distributed transaction as a series of local transactions, each with a compensating action, coordinated through events rather than a single commit.
A single-database transaction can roll back atomically if one step fails. Across independent services — each with its own database — there's no such thing as one atomic commit spanning all of them. If step three of four fails, something has to actively undo steps one and two; nothing does it automatically.
Each service performs its own local transaction and publishes an event when it's done. That event triggers the next service's local transaction (this event-driven chaining is called choreography — see the Orchestration page for the alternative where one coordinator calls each step directly). If any step fails, previously completed steps run a compensating transaction to undo their effect.
Each service only reacts to events; there's no central coordinator. Payment failing triggers a compensating cancellation, chained the same event-driven way.
const bus = new EventTarget();
function orderService() {
bus.addEventListener("OrderRequested", (e) => {
console.log("order: created (pending)");
bus.dispatchEvent(new CustomEvent("OrderCreated", { detail: e.detail }));
});
bus.addEventListener("PaymentFailed", (e) => {
console.log("order: compensating — cancelling order");
});
}
function paymentService() {
bus.addEventListener("OrderCreated", (e) => {
const ok = e.detail.orderId !== "bad-order";
if (ok) {
console.log("payment: charged");
bus.dispatchEvent(new CustomEvent("PaymentCompleted", { detail: e.detail }));
} else {
console.log("payment: declined");
bus.dispatchEvent(new CustomEvent("PaymentFailed", { detail: e.detail }));
}
});
}
orderService();
paymentService();
bus.dispatchEvent(new CustomEvent("OrderRequested", { detail: { orderId: "order-1" } }));
bus.dispatchEvent(new CustomEvent("OrderRequested", { detail: { orderId: "bad-order" } }));