DEV Community

Grafikui
Grafikui Subscriber

Posted on

Sagas in Node.js Without the Heavy Lifting: Introducing Saga Engine

The Saga pattern is a lifesaver for data consistency across microservices, but implementation is often a choice between a mess of nested try/catch blocks or deploying a heavy orchestrator like Temporal.

Saga Engine is a crash-resilient, Postgres-backed executor for Node.js. It provides the core benefits of a distributed transaction coordinator with zero additional infrastructure.


1. The Logic: Clean, Atomic Workflows

We focus on the "No Magic" philosophy. You define steps; the engine guarantees execution or compensation.

await tx.run(async (t) => {
  // Step 1: Reserve inventory
  await t.step('reserve-inventory', {
    idempotencyKey: 'order-123-inv',
    execute: () => inventory.reserve(items),
    compensate: (res) => inventory.release(res.id),
  });

  // Step 2: Process payment via internal gateway
  await t.step('process-payment', {
    idempotencyKey: 'order-123-pay',
    execute: () => paymentGateway.charge(amount),
    compensate: (ch) => paymentGateway.refund(ch.transactionId),
  });
});
Enter fullscreen mode Exit fullscreen mode

What Happens on Failure?
If process-payment fails:

  1. process-payment execution stops immediately.

  2. reserve-inventory.compensate() is automatically triggered using the result from its execute step.

  3. The transaction is marked as failed.


2. Hard Guarantees

These aren't suggestions; they are enforced at the code level.

Guarantee Enforcement
Idempotency Required. Throws IdempotencyRequiredError if keys are missing.
Durability State is persisted to Postgres before the next step starts.
Concurrency Postgres advisory locks prevent double-execution across pods.
Time-Boxed 15-minute hard limit. Designed for high-integrity, short-lived workflows.
Visibility Failed compensations move to a dead_letter state for manual audit.

3. Explicit Refusals

A professional tool is defined by its boundaries. Saga Engine is not for everything:

  • No Workflows > 15 Minutes: Use Temporal. We provide the upgrade path, not the bloat.
  • No Auto-Recovery from Dead Letters: If compensation fails, a human must investigate. Manual intervention via npx saga-admin retry prevents cascading failures.
  • No Distributed Transactions: We coordinate side effects; we do not replace your DB's native ACID properties.
"bin": {
  "saga-admin": "bin/saga-admin.js"
}
Enter fullscreen mode Exit fullscreen mode

4. How It Compares

Feature Saga Engine Temporal BullMQ
Infra Postgres Only Dedicated Cluster Redis
Compensation Native compensate() Manual try/catch Manual
Learning Curve 5 Minutes 1-2 Weeks 2 Days
Best For API Orchestration Long-running/Complex Job Queues

5. Observability & Retries

Plug into your existing stack with 13 built-in hooks:

const tx = new Transaction('order-123', storage, {
  events: {
    onCompensationFailed: (name, err) => alerting.page(`Rollback failed: ${name}`),
    onDeadLetter: (id) => alerting.critical(`Manual intervention required: ${id}`),
  },
});
Enter fullscreen mode Exit fullscreen mode

If a workflow enters dead_letter due to a downstream service outage, fix the issue and use the CLI:

npx saga-admin retry order-123


Quick Start: Setup

To keep your logic clean, move the initialization to your infrastructure layer.

import { Transaction, PostgresStorage, PostgresLock } from 'saga-engine';
import { Pool } from 'pg';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const storage = new PostgresStorage(pool);
const lock = new PostgresLock(pool);

// Use this 'tx' instance in your services
Enter fullscreen mode Exit fullscreen mode

Conclusion

Saga Engine v1.0.0 is production-ready. It fills the gap for teams that need saga semantics without the infrastructure tax.


Questions? Feedback? Found a bug?
Open an issue on GitHub or drop a comment below.

Star on GitHub

npm | GitHub | Docs

Top comments (0)