npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sagalicious

v0.1.1

Published

Orchestration-based Saga pattern implementation for TypeScript. Coordinate distributed transactions across databases, microservices, and APIs with automatic compensation on failure.

Readme

Sagalicious

Orchestration-based Saga pattern for TypeScript Coordinate distributed transactions with automatic compensation

npm version License: MIT TypeScript

Sagalicious is a framework-agnostic TypeScript library that implements the orchestration-based Saga pattern for coordinating distributed transactions with automatic compensation on failure. Coordinate operations across databases, microservices, and APIs with type-safe rollback handlers.

Why Sagalicious?

  • Type-safe - Full TypeScript support with generics
  • Framework-agnostic - Works with Express, NestJS, or any Node.js framework
  • Flexible - Functional or class-based handler definitions
  • Persistent - Optional transaction persistence with custom repositories
  • Battle-tested - Implements proven distributed transaction patterns
  • Zero dependencies - Lightweight and minimal

Installation

npm install sagalicious

Quick Start

import { createSaga } from 'sagalicious';

const saga = createSaga()
  .handler('payment', {
    execute: async (cmd, tx) => {
      await stripe.charges.create({ amount: cmd.amount });
    },
    rollback: async (cmd, tx) => {
      await stripe.refunds.create({ charge: cmd.chargeId });
    }
  })
  .handler('inventory', {
    execute: async (cmd, tx) => {
      await db.inventory.reserve(cmd.itemId);
    },
    rollback: async (cmd, tx) => {
      await db.inventory.release(cmd.itemId);
    }
  })
  .build();

await saga.execute([
  { type: 'payment', amount: 100, chargeId: 'ch_123' },
  { type: 'inventory', itemId: 'item-456' }
]);

If any command fails, rollbacks run in reverse order automatically.

The Saga Pattern

Sagalicious implements the orchestration-based Saga pattern, where a central coordinator (the saga) explicitly controls the flow of execution:

  • Sequential execution - Commands execute in order, one after another
  • Central coordination - The saga orchestrator manages all operations
  • Automatic compensation - Failed transactions trigger rollbacks in reverse order
  • Predictable flow - Easy to understand, debug, and test

This differs from the choreography-based approach where services listen to events and decide independently. Orchestration provides better visibility, simpler error handling, and centralized transaction logic—ideal for coordinating complex operations across multiple services or databases.

Core Concepts

Commands

Commands are plain objects that describe operations:

interface Command {
  type?: string;
  metadata?: Record<string, any>;
}

Handlers

Two ways to define handlers:

Functional API (recommended):

createSaga()
  .handler('payment', {
    execute: async (cmd, tx) => { /* forward */ },
    rollback: async (cmd, tx) => { /* rollback */ }
  })

Class-based API:

class PaymentProcessor implements CommandProcessor<PaymentCommand> {
  canProcess(command: Command): command is PaymentCommand {
    return command.type === 'payment';
  }

  async process(command: PaymentCommand, transaction: Transaction) {
    await chargePayment(command.amount);
  }

  async rollBack(command: PaymentCommand, transaction: Transaction) {
    await refundPayment(command.amount);
  }
}

createSaga()
  .processor(new PaymentProcessor())

Transactions

Transactions track execution state:

interface Transaction<TMetadata = any> {
  id: string;
  status: TransactionStatus;
  commands: Command[];
  metadata?: TMetadata;
  createdAt: Date;
  updatedAt: Date;
}

enum TransactionStatus {
  Pending = 'PENDING',
  Completed = 'COMPLETED',
  RolledBack = 'ROLLED_BACK'
}

API Reference

createSaga()

Creates a builder for configuring the saga.

const saga = createSaga()
  .handler(type, config)      // Add functional handler
  .handler(config)            // Add handler with type in config
  .processor(processor)       // Add class-based processor
  .withRepository(repository) // Add persistence
  .build();

saga.execute()

Executes commands with automatic rollback on failure:

const transaction = await saga.execute(commands, options?);

Parameters:

  • commands: Command[] - Commands to execute
  • options?: TransactionOptions<TMetadata> - Optional configuration

Returns: Transaction<TMetadata> with status Completed

Throws: Original error after automatic rollback

saga.initTransaction()

Initializes a transaction without executing:

const transaction = await saga.initTransaction(commands, options?);

saga.commitTransaction()

Executes all commands in forward order:

await saga.commitTransaction(transaction);

saga.rollBackTransaction()

Executes compensations in reverse order:

await saga.rollBackTransaction(transaction);

Persistence

Global Configuration (Recommended)

Configure repository once at application startup:

import { configureSagalicious, createSaga } from 'sagalicious';
import { MongoTransactionRepository } from './repositories/mongo';

// At app initialization
configureSagalicious({
  repository: new MongoTransactionRepository()
});

// All sagas automatically use the configured repository
const saga = createSaga()
  .handler('payment', { execute, rollback })
  .build();

Per-Saga Configuration

Override global config for specific sagas:

import { InMemoryTransactionRepository } from 'sagalicious';

const saga = createSaga()
  .handler('payment', { execute, rollback })
  .withRepository(new InMemoryTransactionRepository()) // Overrides global
  .build();

Custom Repository

Implement TransactionRepository for your database:

interface TransactionRepository {
  create(transaction: Transaction): Promise<void>;
  findByIdAndUpdate(id: string, updates: Partial<Transaction>): Promise<void>;
  findById(id: string): Promise<Transaction | null>;
  deleteById(id: string): Promise<void>;
  findByStatus(status: TransactionStatus): Promise<Transaction[]>;
}

Transaction Metadata

Attach custom metadata to transactions:

interface OrderMetadata {
  orderId: string;
  customerId: string;
}

const transaction = await saga.execute<OrderMetadata>(
  commands,
  {
    metadata: {
      orderId: 'order-123',
      customerId: 'customer-456'
    }
  }
);

console.log(transaction.metadata.orderId);

Error Handling

try {
  await saga.execute(commands);
} catch (error) {
  if (error instanceof NoProcessorFoundError) {
    console.error('No handler registered for command');
  } else {
    console.error('Transaction failed after rollback:', error);
  }
}

Use Cases

Multi-database transactions

createSaga()
  .handler('postgres', {
    execute: async (cmd) => await postgres.insert(cmd.data),
    rollback: async (cmd) => await postgres.delete(cmd.id)
  })
  .handler('mongodb', {
    execute: async (cmd) => await mongo.insertOne(cmd.data),
    rollback: async (cmd) => await mongo.deleteOne({ _id: cmd.id })
  })

Microservices coordination (Saga pattern)

createSaga()
  .handler('order-service', {
    execute: async (cmd) => await orderService.create(cmd),
    rollback: async (cmd) => await orderService.cancel(cmd.orderId)
  })
  .handler('payment-service', {
    execute: async (cmd) => await paymentService.charge(cmd),
    rollback: async (cmd) => await paymentService.refund(cmd.chargeId)
  })
  .handler('notification-service', {
    execute: async (cmd) => await notify.send(cmd),
    rollback: async (cmd) => {} // Notifications don't need rollback
  })

Mixed operations (DB + API + Events)

createSaga()
  .handler('database', {
    execute: async (cmd) => await db.users.create(cmd.user),
    rollback: async (cmd) => await db.users.delete(cmd.user.id)
  })
  .handler('stripe', {
    execute: async (cmd) => await stripe.customers.create(cmd.customer),
    rollback: async (cmd) => await stripe.customers.del(cmd.customerId)
  })
  .handler('webhook', {
    execute: async (cmd) => await webhooks.trigger('user.created', cmd),
    rollback: async (cmd) => await webhooks.trigger('user.deleted', cmd)
  })

TypeScript

Full type safety with generics:

interface PaymentCommand extends Command {
  type: 'payment';
  amount: number;
  currency: string;
}

class PaymentProcessor implements CommandProcessor<PaymentCommand> {
  canProcess(command: Command): command is PaymentCommand {
    return command.type === 'payment';
  }

  async process(command: PaymentCommand, transaction: Transaction) {
    // command is fully typed as PaymentCommand
    const charge = await stripe.charges.create({
      amount: command.amount,
      currency: command.currency
    });
  }

  async rollBack(command: PaymentCommand, transaction: Transaction) {
    await stripe.refunds.create({ charge: command.chargeId });
  }
}

Testing

Mock handlers for testing:

import { createSaga } from 'sagalicious';

const mockPayment = jest.fn();
const mockRefund = jest.fn();

const saga = createSaga()
  .handler('payment', {
    execute: mockPayment,
    rollback: mockRefund
  })
  .build();

await saga.execute([{ type: 'payment', amount: 100 }]);

expect(mockPayment).toHaveBeenCalledWith(
  { type: 'payment', amount: 100 },
  expect.objectContaining({ status: 'PENDING' })
);

License

MIT