JSPM

sagalicious

0.1.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 6
  • Score
    100M100P100Q46317F
  • License MIT

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

Package Exports

  • sagalicious
  • sagalicious/dist/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (sagalicious) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Sagalicious

Orchestration-based Saga pattern for TypeScript Coordinate distributed transactions with automatic compensation. Built by @tom-dorofeyev

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

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' })
);

About the Name

Sagalicious combines "saga" (the distributed transaction pattern) with "licious" to convey that implementing the Saga pattern can be delightful and elegant. The name reflects the smooth, developer-friendly experience of orchestrating complex distributed transactions with automatic compensation.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Development

# Install dependencies
npm install

# Run tests
npm test

# Build
npm run build

# Lint
npm run lint

Roadmap

  • Timeout support for long-running transactions
  • Retry mechanisms with exponential backoff
  • Transaction visualization/debugging tools
  • Additional repository implementations (PostgreSQL, MongoDB, Redis)
  • Performance metrics and monitoring hooks

Author

Tom Dorofeyev

License

MIT © Tom Dorofeyev


Built with ❤️ for distributed systems developers