← Code Exchange

Temporal E-Commerce Demo

A full-stack e-commerce demo built entirely on Temporal durable execution: cart, checkout, orders, inventory, and fulfillment are TypeScript workflows — declarative state machines around pure, unit-tested deciders. Next.js + Cassandra + Elasticsearch.


Temporal Commerce Demo — Project Description#

A full-stack fragment of an e-commerce application built entirely on Temporal durable execution. Every state transition — from adding an item to a cart through order fulfillment and delivery — is a Temporal workflow. No message queues, no cron jobs, no saga orchestrators. The business logic is the infrastructure.

Stack: Next.js 16 · Temporal TypeScript SDK 1.19 · Apache Cassandra · Elasticsearch Scale: ~19,000 LOC · 6 Temporal workflow domains · 260+ products · 10,600 variants

Note: This demo is derived from a much more comprehensive e-commerce platform under active development. It is a standalone extraction designed to showcase Temporal patterns without the full platform's multi-tenant, multi-supplier, and plugin architecture.

AI Disclosure: AI tooling was used extensively for code generation and documentation. Correctness is enforced by the project's verification gates rather than line-by-line review: a three-level test suite (pure decider unit tests, workflow tests against Temporal's time-skipping test server, and a cross-domain e2e), CI checks for lint / types / formatting / diagram freshness, and custom ESLint rules that enforce the architecture's invariants. The product catalog was created with Printify — real products in a Printify store, exported and adapted for the demo.

Why This Exists#

Every e-commerce system is a distributed state machine. A shopping cart lives in one service, payment processing in another, inventory in a third, and fulfillment in a fourth. The traditional approach wires these together with REST calls, message queues, cron jobs, and reconciliation scripts.

This project demonstrates that Temporal eliminates that entire infrastructure layer. The application has:

  • No message queue — no Kafka, no RabbitMQ, no SQS. Workflow signals replace all async messaging.
  • No cron jobs — the inventory service workflow replaces "run every 5 minutes" with condition(() => dirty, '5m').
  • No dead-letter queues — Temporal's retry policies and activity timeouts handle all transient failures.
  • No saga orchestrator — the checkout workflow is the saga. Steps, compensations, and timeouts are just workflow code.
  • No distributed transaction coordinatorupdateWithStart gives atomic create-or-update. allHandlersFinished gives graceful shutdown.

What This Demonstrates#

Beyond the individual Temporal APIs, the project shows an opinionated way to author durable workflows at scale:

  • Workflows as state machines — domain workflows are written as prepare → decide → finalize loops around pure, unit-tested deciders, driven by a small declarative runStateMachine framework (src/temporal/framework). Effects live in prepare/finalize activities; decisions are pure functions you can test in milliseconds.
  • Cross-domain correlation — every workflow carries a parseable demo.{domain}.{entityId} ID plus correlation Search Attributes, so one Temporal visibility query (CorrelationId = '<cartId>') returns the whole cart → checkout → order → fulfillment journey in the Temporal UI.
  • Transition recording — every state transition is snapshotted to Cassandra with full context, powering a built-in Order Trace dev tool that reconstructs one order's journey across five workflows.
  • Diagrams generated from source — every state machine's Mermaid diagram, per-state trigger table, and the cross-domain orchestration graph are auto-generated from the states config (State Machine Reference) and kept fresh by CI.
  • Three-level testing without Docker — pure decider unit tests, workflow tests on Temporal's time-skipping test server, and a full cart → checkout → OMS → fulfillment e2e. npm test needs no containers.

Architecture#

The Next.js server actions layer is the sole bridge between the browser and the Temporal cluster. Every cart mutation is a Temporal workflow update. Every product query hits Elasticsearch read projections that are kept in sync by workflow activities.

Workflow Domains#

Cart — Durable Entity as a Declarative State Machine#

The cart is a long-running Temporal workflow that acts as a live, queryable entity, orchestrated by the runStateMachine driver. There are no database reads for cart state — the workflow is the cart.

Pattern Implementation
State machine driver All mutations are handled sequentially via a FIFO update queue, preventing write race conditions
Lazy creation updateWithStart atomically creates-or-updates the cart workflow on the first "Add to Cart" click
Live state React UI reads cart state via Temporal queries; mutations are Temporal updates with synchronous return values
Infinite lifetime continueAsNew after 100 updates resets the event history while preserving full cart state
Graceful shutdown await condition(allHandlersFinished) ensures in-flight update handlers complete before continueAsNew
Child orchestration Checkout is started as a child workflow with ABANDON parent close policy

Checkout — Prerequisite-Accumulation State Machine#

Checkout collects shipping and payment as prerequisites in a single collecting state; the UI's shipping → payment → review step is derived from which prerequisites are satisfied rather than encoded as separate workflow states. Order processing runs inline on submit, and a stale-cart guard (reviewedCartVersion) aborts submission if the cart changed after review.

Pattern Implementation
Declarative states Transitions and guards are defined in a states config; a pure decider owns every decision
Derived UI steps deriveStep() maps prerequisites to the wire-visible step, so the storefront contract stays step-based
Price integrity Submitting with a stale reviewedCartVersion aborts with CART_CHANGED
Reservation management Inventory reservations are renewed at checkout start, released on timeout/cancellation, confirmed on success
Timeout condition(() => complete, '1 hour') auto-cancels stale checkouts and releases inventory
Cross-workflow signaling Checkout signals the parent cart workflow with the result via getExternalWorkflowHandle
Activity-driven spawning On order submission, an activity starts the OMS workflow — fully decoupling checkout from order management

Order Management (OMS) — Lifecycle Orchestration#

The OMS workflow manages an order from placement through delivery. It coordinates supplier assignments, tracks fulfillment status, and maintains audit history.

Pattern Implementation
Supplier routing resolveSupplierAssignments activity decides which supplier handles each line item
Decoupled fulfillment Fulfillment is started via an activity (not startChild), making it a standalone workflow with its own lifecycle
Signal-driven updates Fulfillment status flows upward via signals — the OMS aggregates across all supplier orders to derive order-level status
Status projections Every status change is indexed to Elasticsearch for real-time admin panel updates
Audit trail Every status transition is recorded in the order_status_history Cassandra table

Fulfillment — Strategy-Based State Machine#

The fulfillment workflow receives pre-decided supplier orders and executes the appropriate fulfillment strategy for each, driven by the same declarative state machine framework. Fulfiller-order children keep running when their parent closes.

Pattern Implementation
State orchestration Simulated orders move through received → submitting → in_production → shipped → delivered via the state machine loop
Automatic mode wf.sleep() timers simulate processing → shipping → delivery
Manual mode Feature flag MANUAL_FULFILLMENT=true pauses at each stage, waiting for Temporal signals to advance
Inventory lifecycle Reservations are transferred to supplier on start, fulfilled on delivery, released on rejection
Email notifications Shipped and delivered emails are sent via activity stubs

Inventory — CQRS Event Processor#

The inventory service is a single long-running workflow that replaces an entire message queue consumer + cron job infrastructure.

Pattern Implementation
Signal-driven projections Write-side mutations signal the inventory service with changed SKUs; it runs targeted read-side projections
Dirty-flag batching Rapid-fire mutations result in a single projection pass, not one per mutation
Dual-trigger condition(() => dirtySkus.size > 0, '5m') gives both event-driven and time-driven behavior
Lazy start signalWithStart creates the inventory service on the first inventory mutation
Reservation lifecycle Temporary → Confirmed → Fulfilled/Released, with TTL-based expiration

Identity — Shopper Authentication and Address Persistence#

The identity domain provides email-based shopper authentication and saved shipping addresses — a password-less, demo-focused system where accounts are auto-created on first login.

Pattern Implementation
Email-only auth No passwords — POST /api/auth/shopper/login auto-creates accounts on first login
Cookie sessions shopperId cookie persists the session across page loads (30-day TTL)
Guest-to-member promotion Guest shoppers who complete checkout are automatically promoted to members using the shipping address email
Address pre-fill Returning shoppers have their checkout shipping form pre-populated from saved default addresses
Order lookup /shop/orders lets signed-in shoppers view order history with full shipping details

Data Architecture#

Write Side — Cassandra#

Cassandra serves as the durable write store with partition-key isolation:

Table Family Purpose
products, variants, collections Product catalog
orders, orders_by_customer, orders_by_confirmation Order persistence (3 denormalized views)
order_status_history Audit trail (TimeUUID clustering)
inventory_stock_w, inventory_reservations_w Inventory state
shoppers, shopper_shipping_addresses Shopper accounts and saved addresses

Every workflow state transition is also snapshotted to Cassandra with full context — this powers the Order Trace tool below.

Read Side — Elasticsearch#

Elasticsearch serves as the read projection layer with full-text search and faceted filtering across 11 indices — products and collections for the storefront; orders, supplier_orders, customers, inventory, carts, reservations, fulfillments, shipments, and suppliers for the admin panel and its Elasticsearch explorer.

Unified Worker Architecture#

All six domain workers run in a single Node.js process, sharing one gRPC connection to Temporal. Each domain has its own task queue, workflow registrations, and activity implementations.

Task queue isolation means a slow fulfillment activity cannot block cart operations; in production the domains can be split into separate deployments for independent scaling.

Key Temporal Patterns Demonstrated#

# Pattern Where Used
1 Declarative state machine (runStateMachine) around pure deciders Cart, Checkout, OMS, Fulfillment
2 updateWithStart — atomic lazy entity creation Cart
3 Query/Update handlers — workflow as live entity Cart, Checkout, OMS
4 continueAsNew — infinite entity lifetime Cart, Inventory Service
5 Parent-child with ABANDON policy Cart → Checkout
6 condition() with timeout — reservation TTL Checkout, Inventory
7 Cross-workflow signaling via getExternalWorkflowHandle Checkout → Cart, Fulfillment → OMS
8 Activity-driven workflow spawning (not startChild) OMS → Fulfillment, Checkout → OMS
9 Signal-driven status propagation Fulfillment → OMS → Elasticsearch
10 Workflow as CQRS event processor Inventory Service
11 Correlation Search Attributes — one visibility query per journey All domains
12 Shared connection, isolated task queues Unified Worker
13 Dirty-flag projection batching Inventory, OMS

Error Handling — Redemptive State Recovery#

When a workflow operation fails, the system returns to the last known good state instead of crashing:

  • Payment failure → checkout returns to collecting with an error message; reservations are kept for a submit retry (they expire via the inventory TTL)
  • Checkout timeout → reservations released, cart returns to active
  • Terminal workflow → server action wrapper catches WorkflowNotFoundError and returns null for graceful UI degradation
  • Worker crash → Temporal automatically replays the workflow from the last checkpoint; no state is lost

Observability#

  • Order Trace dev tool (/dev/order-trace) — reconstructs one order's full cross-domain lifecycle (cart → checkout → OMS → fulfillment → fulfiller order) from the state transitions recorded to Cassandra.
  • Temporal UI correlationCorrelationId = '<cartId>' in the visibility query returns every workflow in the journey.
  • Opt-in tracing/metrics stack — Jaeger, Prometheus, and Grafana via npm run infra:up:obs (or OTEL_ENABLED=true).

Quick Start#

Prerequisites: Node.js ≥ 22 and Docker.

Resource URL
Storefront http://localhost:3000/shop
Admin Panel http://localhost:3000/admin
Temporal UI http://localhost:8233
Order Trace (dev tool) http://localhost:3000/dev/order-trace

Demo Limitations#

Deliberate simplifications that keep the focus on the Temporal patterns: payments are mocked, emails are stubbed activities that log instead of send, the /admin area is intentionally unauthenticated, and fulfillment is a timer-driven simulation.

Technology Stack#

Layer Technology Purpose
Frontend Next.js 16 (App Router), React, Tailwind CSS Server-rendered storefront and admin panel
Server Next.js Server Actions + API Routes Bridge between browser and Temporal cluster
Orchestration Temporal TypeScript SDK Durable workflow execution for all state transitions
Write Store Apache Cassandra Partition-key-isolated persistence for catalog, orders, inventory
Read Store Elasticsearch Full-text search, faceted filtering, CQRS read projections
Infrastructure Docker Compose Local development; deploys to Temporal Cloud + Google Cloud Run

Documentation#


Language

TypeScript
💖 Community

About the Author

Jeff Romine gravatar

Jeff Romine

Night Heron Software