HazelJS Saga Package
@hazeljs/saga provides distributed transaction management for HazelJS applications. It simplifies the implementation of complex, multi-service workflows using either the Orchestration (centralized) or Choreography (event-driven) model, ensuring cross-module data consistency with automated compensation logic.
Quick Reference
- Purpose: Coordinate multiple service operations as a single unit or transaction across distributed systems.
- When to use: Use when your business logic spans multiple microservices or modules (e.g., Order → Inventory → Payment → Shipping) and you need to ensure all operations succeed or all stay rolled back (compensation).
- Key concepts:
@Sagaorchestrator,@SagaStep,@SagaChoreography,@OnEvent,SagaOrchestrator,SagaChoreography,SagaContext, status tracking (STARTED, COMPENSATING, ABORTED, COMPLETED). - Inputs: Initial event or service data for the transaction.
- Outputs:
SagaContextwith the final status and step outputs. - Dependencies:
@hazeljs/core,@hazeljs/event-emitter. - Common patterns: Orchestration for complex, multi-step, centralized workflows; Choreography for decentralized, event-driven, loosely coupled flows.
- Common mistakes: Forgetting to implement idempotent compensation methods; not tracking Saga state in production (use Redis); coupling the Saga Orchestrator too tightly.
Architecture Mental Model
graph TD A["Request Start Order"] --> B["Saga Orchestrator"] B -- "Step 1" --> C["Reserve Inventory"] B -- "Step 2" --> D["Charge Payment"] B -- "Step 3" --> E["Ship Order"] D -- "Failure!" --> F["Rollback Pending"] F -- "Compensate 1" --> G["Cancel Reservation"] style B fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff style C fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff style D fill:#ef4444,stroke:#f87171,stroke-width:2px,color:#fff style G fill:#fbbf24,stroke:#fcd34d,stroke-width:2px,color:#fff
When to Use @hazeljs/saga
| Scenario | Use Model | Benefit |
|---|---|---|
| Multi-step checkout flow | Orchestration | Centralized control and visibility. |
| Loosely coupled service updates | Choreography | Decoupled event-driven flows. |
| External infrastructure creation | Orchestration | Automated rollback on failure. |
| Complex data migration/ETL | Choreography | Distributed processing with tracking. |
Installation
npm install @hazeljs/saga @hazeljs/event-emitter
HazelJS SagaModule Registration
Register the SagaModule and EventEmitterModule in your root or feature module.
src/app.module.ts
import { HazelModule, EventEmitterModule } from '@hazeljs/core';
import { SagaModule } from '@hazeljs/saga';
@HazelModule({
imports: [
EventEmitterModule,
SagaModule.forRoot({
backend: 'redis',
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: 6379,
},
}),
],
})
export class AppModule {}
Saga Orchestration (Centralized)
Use orchestration for complex, multi-step workflows where you need a single point of truth.
Defining the Saga
import { Saga, SagaStep, SagaContext } from '@hazeljs/saga';
@Saga({ name: 'create-order' })
export class OrderSaga {
@SagaStep({ order: 1, compensate: 'cancelReservation' })
async reserveInventory(ctx: SagaContext) {
return await this.inventory.reserve(ctx.input.productId, ctx.input.qty);
}
@SagaStep({ order: 2, compensate: 'refundPayment' })
async processPayment(ctx: SagaContext) {
return await this.payment.charge(ctx.input.amount);
}
async cancelReservation(ctx: SagaContext) {
await this.inventory.cancel(ctx.input.productId, ctx.input.qty);
}
async refundPayment(ctx: SagaContext) {
await this.payment.refund(ctx.input.amount);
}
}
Executing the Saga
Inject the SagaOrchestrator to start your distributed transaction.
import { Service } from '@hazeljs/core';
import { SagaOrchestrator } from '@hazeljs/saga';
@Service()
export class OrderService {
constructor(private readonly orchestrator: SagaOrchestrator) {}
async create(data: OrderDto) {
const result = await this.orchestrator.start('create-order', data);
if (result.status === 'failed') {
throw new Error(`Order failed! Status: ${result.status}`);
}
return result.outputs;
}
}
Saga Choreography (Decentralized)
Choreography is entirely event-driven. Handlers subscribe to events and emit new ones to trigger the next phase of the transaction.
import { SagaChoreography, OnEvent } from '@hazeljs/saga';
@SagaChoreography()
export class DeliveryHandler {
@OnEvent('order:payment:completed')
async initiateShipping(event: PaymentCompletedEvent) {
// Logic to start delivery
return { status: 'success', event: 'order:shipping:started' };
}
@OnEvent('order:payment:failed', { type: 'compensate' })
async handlePaymentFailure(event: PaymentFailedEvent) {
// Manual compensation for choreography (rollback)
await this.shipping.cancel(event.orderId);
}
}
Saga Context and Lifecycle Statuses
The SagaContext tracks the status of the transaction as it progresses:
| Status | Description |
|---|---|
STARTED | Execution has begun. |
COMPLETED | All steps finished successfully. |
FAILED | A forward step encountered an error; compensation is pending. |
COMPENSATING | Reversing previously completed steps in reverse order. |
ABORTED | Compensation finished; the transaction is fully rolled back. |