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),
});
});
What Happens on Failure?
If process-payment fails:
process-paymentexecution stops immediately.reserve-inventory.compensate()is automatically triggered using the result from itsexecutestep.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 retryprevents 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"
}
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}`),
},
});
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
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.
Top comments (0)