SapixDBSapixDB/Docs
Home
Enterprise · Distributed

Saga Transactions

◆ EnterpriseRequires the enterprise build. Community builds return 404 on these endpoints.

Write to multiple agents atomically. SapixDB executes each step in sequence and automatically compensates all applied steps if anything fails — no two-phase commit coordinator, no distributed locks.

⬡
Automatic compensation
If any step fails, SapixDB writes a TOMBSTONE record to every previously-applied step in reverse order — no manual rollback code needed.
⚡
Synchronous execution
POST /v1/saga blocks until all steps commit or all applied steps are compensated. One HTTP call, one definitive outcome.
◉
Cross-instance writes
Each step targets the primary agent of any SapixDB HTTP endpoint — a different instance, a different cloud region, or a different process on the same host. (Not a named secondary agent within one instance — see Limitations.)
⚖
Durable state
Saga state is persisted to RocksDB graph-meta before execution begins. Crashes do not silently lose saga records.
⚠ Enterprise featureSaga Transactions require the saga feature flag. This is available on the Enterprise plan. Community builds return 404 on saga routes.

State machine

A saga moves through these states exactly once:

saga state transitions
Pending → Executing → Committed            (all steps applied)
                    → Compensating → Compensated   (all applied steps rolled back)
                                  → Failed          (compensation itself failed)
StateHTTP statusMeaning
pending—Created, execution not yet started
executing—Steps are being applied one by one
committed201 CreatedAll steps applied — the happy path
compensating—A step failed; rolling back applied steps
compensated207 Multi-StatusAll applied steps successfully reversed
failed207 Multi-StatusCompensation itself failed for at least one step

API

Create and execute a saga

A single POST creates and immediately executes the saga. The request blocks until all steps commit or all applied steps are compensated.

POST /v1/saga
POST /v1/saga
Authorization: Bearer <key>
Content-Type: application/json

{
  "steps": [
    {
      "target_agent_url": "http://analytics:7475",
      "payload_b64": "<base64-msgpack>",
      "flags": 0
    },
    {
      "target_agent_url": "http://billing:7475",
      "payload_b64": "<base64-msgpack>",
      "flags": 0
    }
  ]
}
FieldTypeDescription
target_agent_urlstringHTTP base URL of the target SapixDB instance
payload_b64stringBase64-encoded MessagePack record payload
flagsu8BlockFlags bitmask — 0 = plain data, 2 = TOMBSTONE

Response — 201 Created on full commit, 207 Multi-Status on compensation or failure:

response
{
  "id": "a1b2c3d4e5f6a7b8",
  "coordinator_agent_id": "orders",
  "state": "committed",
  "created_at_ms": 1754478000000,
  "completed_at_ms": 1754478000042,
  "steps": [
    {
      "step_id": "step_1",
      "target_agent_url": "http://analytics:7475",
      "payload_b64": "...",
      "flags": 0,
      "status": "applied",
      "record_hash": "a3f9c2e1...",
      "compensation_hash": null,
      "error": null
    }
  ]
}

Step status values

StatusMeaning
pendingNot yet attempted
appliedRecord written to target agent
compensatedTOMBSTONE written to target agent (step rolled back)
failedApply or compensation failed — check the error field

List all sagas

GET /v1/saga
GET /v1/saga
Authorization: Bearer <key>

// Response
{
  "sagas": [ ... ],
  "total": 5
}

Returns newest-first.

Get one saga

GET /v1/saga/:id
GET /v1/saga/a1b2c3d4e5f6a7b8
Authorization: Bearer <key>

Returns a single SagaTransaction object. Returns 404 if not found.

How compensation works

When a step fails, SapixDB iterates all applied steps in reverse order and writes a TOMBSTONE record (flags: 2) to each one. The compensation payload identifies the original record being reversed:

compensation payload
{
  "_saga_compensation": true,
  "saga_id": "a1b2c3d4e5f6a7b8",
  "compensating_for": "<original_record_hash>"
}

Steps that were never applied (pending) are skipped — only applied steps get compensated. The compensation_hash on each step is set to the content hash of the TOMBSTONE record written for it.

ℹ TOMBSTONE semanticsA TOMBSTONE is a SapixDB record with flags: 2. It does not delete the original record — it appends a marker record to the strand identifying the original by content hash, and the original always remains in the immutable chain either way. current/count_current queries (including index-accelerated ones) recognize compensating_forand exclude the compensated record from their collapsed "latest per key" results, same as any other tombstone (fixed 2026-09-30). latestis a raw chronological feed by design and is unaffected — it shows the TOMBSTONE marker as its own event alongside the original write, not a collapsed view. "Compensated" means the marker was written successfully; it does not mean application-level effects derived from that write — an aggregate total, a materialized counter, a value already read and acted on elsewhere — are automatically reversed. Your own read-side logic still decides what compensation means for those.

Quick start

This example writes an order event to two agents atomically. If the billing write fails, the analytics write is automatically compensated.

TypeScript
import { encode } from "@msgpack/msgpack";

async function writeSaga(analyticsUrl: string, billingUrl: string) {
  const analyticsPayload = Buffer.from(
    encode({ event: "order_placed", order_id: "ord_123", amount: 4999 })
  ).toString("base64");

  const billingPayload = Buffer.from(
    encode({ charge: 4999, order_id: "ord_123", currency: "usd" })
  ).toString("base64");

  const res = await fetch(`${SAPIX_URL}/v1/saga`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      steps: [
        { target_agent_url: analyticsUrl, payload_b64: analyticsPayload, flags: 0 },
        { target_agent_url: billingUrl,   payload_b64: billingPayload,   flags: 0 },
      ],
    }),
  });

  const saga = await res.json();

  if (saga.state !== "committed") {
    throw new Error(`Saga ${saga.id} ended in state: ${saga.state}`);
  }

  return saga.id;
}

Limitations

Synchronous and blocking
The HTTP request blocks until all steps settle. Set your client timeout based on the number of steps × expected network latency. Each step has a 10-second internal timeout.
At-most-once compensation
If a compensation POST itself times out, that step is left as failed and the saga transitions to the failed state. There is no automatic retry for compensations.
No idempotency key
Submitting the same saga twice creates two separate transactions. Guard against duplicates at the caller (e.g. check for an existing committed saga with the same business key before submitting).
Coordinator single point of recovery
If the coordinator instance restarts mid-execution, the in-flight saga is not automatically resumed. The saga record is durable; recovery tooling can read it, but execution must be re-triggered manually.
Primary agent writes only
Each step writes to POST {url}/v1/records on the target instance's primary agent. Writes to a named secondary agent within a target instance are not supported per step.
← Distributed ModeMulti-Region Replication →