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 coordinator —
updateWithStartgives atomic create-or-update.allHandlersFinishedgives 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
runStateMachineframework (src/temporal/framework). Effects live inprepare/finalizeactivities; 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 testneeds 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
WorkflowNotFoundErrorand returnsnullfor 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 correlation —
CorrelationId = '<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(orOTEL_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#
- Getting Started — clone-to-running setup, including Apple Silicon troubleshooting
- Project Description — the full architecture narrative
- Temporal Lessons Learned — 25 practical lessons from building on the Temporal TypeScript SDK
- State Machine Reference — auto-generated Mermaid diagrams + trigger tables for every workflow
- Demo Instructions — 4–5 minute live demo script
- Cloud Deployment — Temporal Cloud + Google Cloud Run
Language
About the Author

Jeff Romine
Night Heron Software