HazelJS Saga Package

npm downloads

@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: @Saga orchestrator, @SagaStep, @SagaChoreography, @OnEvent, SagaOrchestrator, SagaChoreography, SagaContext, status tracking (STARTED, COMPENSATING, ABORTED, COMPLETED).
  • Inputs: Initial event or service data for the transaction.
  • Outputs: SagaContext with 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

ScenarioUse ModelBenefit
Multi-step checkout flowOrchestrationCentralized control and visibility.
Loosely coupled service updatesChoreographyDecoupled event-driven flows.
External infrastructure creationOrchestrationAutomated rollback on failure.
Complex data migration/ETLChoreographyDistributed 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:

StatusDescription
STARTEDExecution has begun.
COMPLETEDAll steps finished successfully.
FAILEDA forward step encountered an error; compensation is pending.
COMPENSATINGReversing previously completed steps in reverse order.
ABORTEDCompensation finished; the transaction is fully rolled back.