Skip to main content

Build a Checkout That Handles Failure

An online shop needs to reserve stock, charge a customer, and arrange delivery. The happy path is three service calls. The difficult part starts when a payment gateway is temporarily unavailable or a courier rejects a shipment after the customer has paid. In this tutorial, we’ll build that checkout with Siglo, a TypeScript workflow library that runs inside your Node.js application. We’ll retry a temporary payment failure, undo completed work when shipping fails, and test both outcomes. The demo uses fake services, so you can run it without accounts, API keys, Docker, or a database. By the end, you’ll know what a step is, how data moves between steps, and where a workflow library helps in an ordinary backend.

1. The Problem: A Few Service Calls Become Recovery Logic

A first checkout implementation often looks like this sketch:
Now consider what can go wrong: You can implement these rules with functions, loops, and try/catch. Siglo becomes useful when those rules repeat across business processes: each operation becomes a named step, and a flow defines their order. Retry and recovery policies live beside the operation they belong to. This is still your application calling your services. Siglo coordinates those calls; your inventory system, payment provider, and database remain responsible for their own records.

2. Set Up a Small Project

Use Node.js ^22.13.0 || >=24.0.0 and TypeScript 5 or later. In an empty directory, run:
The files below use the CommonJS default created by npm init -y; tsx runs the TypeScript directly. No build configuration is needed for this demo. Already working in this repository? The completed files are in examples/src/getting-started. With the repository dependencies installed, build core and run:

3. Define the Checkout

Create checkout.ts with the following code. The service interface describes the business operations without tying the workflow to a particular provider. This lets us use fake services now and real API clients later.
Let’s unpack the parts that matter.

A step receives context and returns new fields

Step.create('reserveStock', action) gives an operation a name. Its action receives the order and returns { reservationId }. Siglo adds returned fields to the accumulated context. Later steps can read both the original order and earlier results: Return the fields you produce instead of mutating the input. Prefer distinct names such as paymentId and trackingId so separate operations don’t accidentally overwrite one another’s data.

A flow defines the business sequence

The last line, flow(validateOrder, reserveStock, chargePayment, arrangeShipping), says what happens and in which order. It doesn’t call the services until we run the flow. The validation step runs first, so an invalid order fails before stock is reserved. A step failure stops forward execution unless its failure policy recovers.

Retry only the operation that can recover

The payment step uses .onFail(...) to retry TemporaryPaymentError. maxRetries: 2 allows an initial attempt plus up to two retries, with exponential backoff starting at 10 milliseconds. The short delay keeps this local demo quick; choose delays appropriate to the provider in a real integration. The predicate matters: a declined card won’t become valid after another attempt. It should stop the checkout instead. Our fake gateway fails before accepting the first charge; an ambiguous network timeout after a provider accepts payment needs provider-side idempotency and reconciliation, discussed below.

Compensation undoes completed business operations

.compensate(...) registers a recovery action for a successfully completed step. If shipping fails after payment succeeds, Siglo calls the payment compensation to refund the charge, then the stock compensation to release the reservation. That reverse order is called a saga. It’s useful when several independent services cannot share a database transaction. A refund is a new business operation, so the original charge can still appear in the customer’s payment history. If payment fails before completing, only the stock reservation needs compensation. The payment step has no successful output to refund.

Explore the workflow in your IDE

Siglo provides extensions for VS Code and IntelliJ IDEA. They turn JavaScript and TypeScript workflow definitions into interactive graphs without executing your application. You can inspect step input contracts, retries, timeouts, and compensation policies, click steps to navigate to their source, and refresh the graph as you edit with Auto-Reload. Use All Flows to explore multiple workflows in one file. Follow the linked guides to build and install the VS Code VSIX or IntelliJ plugin ZIP, including each IDE’s analyzer requirements. Open and trust your project, then open checkout.ts:
  • VS Code: open the Command Palette and run Siglo: Open Workflow Visualizer.
  • IntelliJ IDEA: right-click the editor and choose Visualize Siglo Workflow, or open View → Tool Windows → Siglo Workflows.
The graph shows the workflow’s declared structure and policies. To inspect actual runs, retries, and failures, continue with Serve a REST Endpoint and Watch It Run Live.

4. Run the Happy Path and a Failure

Create checkout-demo.ts alongside checkout.ts:
Run it:
Expected output:
On the first run, payment recovers on its second attempt. Stock is reserved once, and shipping proceeds. result.trackingId is available directly because .run(..., { pojo: true }) returns plain object context. On the second run, shipping fails. The log shows the refund before the stock release. The flow still rejects after successful compensation: recovery cleaned up the partial checkout, but the order was never fulfilled. Each demo run creates fresh fake services. The attempt counter and event log exist only to make the behavior visible; real integrations would use your service clients and their persistent records. In production, calculate the amount from trusted catalog or order data before charging a customer.

5. Test the Recovery Rule

A useful checkout test checks the business outcome, including cleanup order. Create checkout.test.ts:
Run it with Node’s test runner:
The complete test file also checks payment recovery, invalid orders, permanent payment failure, and duplicate completed requests. Because services are passed into createCheckout, tests can substitute behavior without contacting external APIs. For step spies, recorded traces, and replay tools, continue with Testing and Replay.

6. Handle a Duplicate Checkout Request

Retries inside a step and duplicate incoming requests are separate problems. A browser can submit twice even when every service call succeeds. At the application boundary, give repeated submissions of the same logical checkout a stable idempotency key. With a checkout created as above, these two calls illustrate reuse of a completed result:
Use the same key only for the same logical request. If the cart changes, validate the request against the stored order and define whether it represents a new checkout. The default idempotency store is in memory. For multiple application instances or restarts, configure a shared persistent store such as PostgreSQL or Redis. Workflow deduplication also cannot close the gap between “the payment provider accepted the charge” and “the application recorded success.” Pass a stable provider-supported idempotency key to the actual payment API, and reconcile unknown outcomes before charging again. See Idempotency and Deduplication for store configuration and step-level policies.

7. Apply the Same Idea to Other Problems

Checkout is one example. Look for a business operation with several stages and explicit rules for failure or waiting. For durable approvals, persistence and resumption must be configured explicitly. If a wait needs to expire while no worker is running it, configure a scheduler or an external dispatch mechanism. An in-memory demo does not survive a process restart.

8. Decide Whether Siglo Fits

Siglo fits when an application needs reusable business steps with retries, compensation, branching, or waits, and you want to define those rules in TypeScript inside the existing service. A single database transaction should usually stay a database transaction. A short function with no recovery rules can stay a function. A queue still handles delivery and worker scheduling; Siglo can coordinate the operations inside its consumer. For this checkout, the next production work is concrete: replace fake clients, add provider idempotency, configure shared storage where required, and observe failures. If a refund itself fails, handle the reported compensation failure and arrange reconciliation; registering a handler does not guarantee that an external refund succeeds. Start with Compensation and Sagas, then Production Operations.

9. Where to Go Next