| name | saga-architecture |
| description | Saga coordination patterns for distributed transactions with compensating actions. |
| origin | FlowDeck |
saga-architecture
When to Activate
When coordinating distributed operations across multiple services or data stores where ACID transactions are not available and compensating actions are needed to maintain eventual consistency.
Steps
- Identify the saga participants - Determine which services or components participate in the distributed operation.
- Define the saga choreography or orchestration - Choose whether sagas will be choreographed (event-driven) or orchestrated (central coordinator).
- Define each step with corresponding compensation - For every forward action, define what compensating action undoes it.
- Implement idempotent operations - Ensure each step can be safely retried and compensation can be safely reapplied.
- Handle saga failures with compensation - On failure, execute compensations in reverse order (for orchestrating sagas) or react to failure events (for choreographing sagas).
- Persist saga state - Store saga state to survive process crashes and enable recovery.
- Add timeout and retry logic - Detect stuck sagas and advance or compensate accordingly.
Examples
interface SagaState<T> {
id: string
currentStep: number
data: T
status: 'pending' | 'in_progress' | 'completed' | 'compensating' | 'failed'
}
class OrderProcessingSaga {
private readonly steps: SagaStep[]
constructor(
private readonly sagaOrchestrator: SagaOrchestrator,
private readonly inventoryService: InventoryService,
private readonly paymentService: PaymentService,
private readonly shippingService: ShippingService
) {
this.steps = [
{
name: 'reserve_inventory',
execute: (state) => this.inventoryService.reserve(state.orderId, state.items),
compensate: (state) => this.inventoryService.release(state.orderId, state.items)
},
{
name: 'process_payment',
execute: (state) => this.paymentService.charge(state.orderId, state.total),
compensate: (state) => this.paymentService.refund(state.orderId, state.total)
},
{
name: 'initiate_shipping',
execute: (state) => this.shippingService.createShipment(state.orderId),
compensate: (state) => this.shippingService.cancelShipment(state.shipmentId)
}
]
}
async execute(orderId: string): Promise<void> {
const state: SagaState<OrderSagaData> = {
id: generateId(),
currentStep: 0,
data: { orderId, items: [], total: 0 },
status: 'in_progress'
}
await this.sagaOrchestrator.start(state, this.steps)
}
}
class OrderCreatedHandler {
constructor(private readonly eventBus: EventBus) {}
async handle(event: OrderCreatedEvent): Promise<void> {
try {
await this.inventoryService.reserve(event.orderId, event.items)
this.eventBus.publish(new InventoryReservedEvent(event.orderId))
} catch (error) {
this.eventBus.publish(new InventoryReservationFailedEvent(event.orderId, error.message))
}
}
}
class InventoryReservedHandler {
async handle(event: InventoryReservedEvent): Promise<void> {
try {
await this.paymentService.charge(event.orderId, event.total)
this.eventBus.publish(new PaymentProcessedEvent(event.orderId))
} catch (error) {
this.eventBus.publish(new InventoryReleaseRequestedEvent(event.orderId))
}
}
}
class PaymentService {
async charge(orderId: string, amount: Money): Promise<TransactionId> {
const existingTx = await this.transactionRepo.findByOrderId(orderId)
if (existingTx) {
return existingTx.id
}
const transaction = await this.paymentGateway.charge(amount)
await this.transactionRepo.save({ orderId, transaction })
return transaction.id
}
}
Related Skills
- clean-architecture
- hexagonal-architecture
- ddd-architecture
- backend-patterns