Transaction — Multi-Step Workflows & Rollbacks
5.24 kB minified + gzipped
Business mutations often span multiple external services — such as reserving inventory, charging a credit card, and creating a shipping manifest. Because these operations cross network boundaries, they cannot run inside a single database transaction.
When a later step fails, developers must manually undo all preceding operations:
This manual approach introduces three failure modes:
- Context Drift: Intermediate values (like
reservationandpayment) must be declared aslet ... | undefinedin an outer scope, forcing non-null assertions (!) throughout the workflow. - Rollback Fault Masking: If a rollback step throws an exception or network error, it can crash the cleanup loop early, leaving earlier resources permanently orphaned.
- Cancellation Leaks: If the client aborts their HTTP connection midway via an
AbortSignal, already-completed steps are often abandoned without running their compensating actions.
Transaction solves this by turning multi-step workflows into immutable execution plans with
accumulated context and guaranteed Last-In, First-Out (LIFO) rollback.
flowchart TB
Start(["Transaction.run(input)"]) --> Step1["Step 1: Reserve Inventory (run)"]
Step1 -->|"Ok(reservation)"| Step2["Step 2: Charge Payment (run)"]
Step2 -->|"Ok(payment)"| Step3["Step 3: Dispatch Shipment (run)"]
Step3 -->|"Ok(shipment)"| Commit(["Commit: Return Ok(Acc)"])
Step3 -->|"Err or Abort"| Undo2["Undo Step 2: Refund Payment (undo)"]
Undo2 --> Undo1["Undo Step 1: Release Inventory (undo)"]
Undo1 --> RollbackExit(["Return Err(TransactionFailure)"])
Type Structure & Mental Model
Section titled “Type Structure & Mental Model”A Transaction tracks four generic channels across its compiled pipeline:
In: The initial payload passed toTransaction.run(input). If no input is required, defaults tovoid.Acc: The accumulated dictionary of completed step outputs. Every successful step adds its declaredidas a typed key to this dictionary.E: The union of forward execution errors across all registered steps.EUndo: The union of errors that can occur during compensatingundoactions.
When executed, a transaction returns a lazy task resolving to either a successful accumulator record or a structured failure:
Defining Steps & Context Accumulation
Section titled “Defining Steps & Context Accumulation”A transaction is created with Transaction.make() and populated with sequential steps via
Transaction.step.
Each step declares a unique id, a forward run function, and an optional compensating undo
function. Every step receives an accumulated context containing the initial payload and all outputs
from preceding steps:
TypeScript verifies context properties statically. If a step attempts to read from a step that has not run yet, or if a duplicate step identifier is used, the compiler rejects the code at build time.
Executing a Transaction & Handling Outcomes
Section titled “Executing a Transaction & Handling Outcomes”To execute a transaction, pass the input payload to Transaction.run. The returned task evaluates
to a Task.Result resolving to Result.Ok(Acc) on success or Result.Err(Transaction.Failure) on
failure:
Automatic LIFO Rollback & Best-Effort Drain
Section titled “Automatic LIFO Rollback & Best-Effort Drain”When any forward step returns an error, throws an unhandled exception, or aborts:
- Forward step progression terminates immediately.
- Completed steps are rolled back in strict reverse chronological order: Step K - 1, then Step K - 2, down to Step 1.
- Steps without an
undohandler are safely skipped during the rollback loop.
Rollback Resilience (Best-Effort Drain)
Section titled “Rollback Resilience (Best-Effort Drain)”In production distributed systems, a network glitch while undoing Step 2 must never prevent Step 1
from being undone. Transaction enforces a best-effort drain: if an undo action fails or throws,
the error is captured in undoErrors, and the rollback loop continues until every completed step
has been processed.
Consider a multi-step media publishing workflow:
If catalog fails, transcode and storage are compensated in reverse order. If cancelling the
transcoder job encounters a network error, mediaCdn.deleteFile still executes, and the final
failure object attributes the rollback issue to undoErrors.
Forward Resilience: Retries & Timeouts
Section titled “Forward Resilience: Retries & Timeouts”Transient network errors (such as HTTP 429 rate limits or socket resets) should often be retried
before triggering an expensive multi-service rollback. Each step can configure an optional
RetryPolicy and execution timeout:
The step retries according to the exponential backoff schedule before failing. Only if all retries are exhausted or the 4-second timeout expires does the step fail and initiate reverse rollback.
Cancellation with AbortSignal
Section titled “Cancellation with AbortSignal”Pass an AbortSignal to cancel in-flight transactions when an HTTP request disconnects or a user
navigates away:
When an abort triggers mid-execution:
- Forward progress for the active step is aborted immediately.
- All previously completed steps are rolled back in reverse (LIFO) order.
- The transaction resolves to
Err(TransactionFailure)with the abort signal reason.
Execution Telemetry Hooks
Section titled “Execution Telemetry Hooks”Transaction.run accepts non-blocking telemetry hooks for audit trails, structured logging, and
metrics:
Telemetry errors are isolated internally — an uncaught error in a logging callback will never disrupt forward execution or halt rollback compensation.
Problems it solves
Section titled “Problems it solves”- Inconsistent multi-service state: When mutations span multiple remote APIs (payment gateways,
inventory databases, third-party logistics), single-database ACID transactions cannot protect
them.
Transactionprovides all-or-nothing consistency through automatic reverse rollbacks. - Fragile manual rollback stacks: Hand-rolled cleanup arrays (
const rollbacks = []) require verbose, repetitivetry/catchboilerplate and are prone to bugs when intermediate errors abort the rollback loop prematurely. - Context threading boilerplate: Later steps frequently depend on IDs generated by earlier
steps.
Transactionautomatically threads step outputs into an accumulated{ initial } & Acccontext with full static type safety. - Resource leaks on client abort: Long-running workflows that accept an
AbortSignaloften drop execution without cleaning up acquired locks or reserved inventory.Transactionguarantees that cancelling an in-flight workflow initiates full LIFO compensation of all completed steps. - Unattributed rollback failures: If an undo action fails in vanilla code, the error is often
swallowed or overwrites the root cause.
Transaction.Failurepreserves both the forward trigger error and every attributed rollback failure inundoErrors.