Skip to content

Temporal E-Commerce Demo #108

@night-heron-software

Description

@night-heron-software

Project link

https://github.com/night-heron-software/temporal-commerce-demo

Language

TypeScript

Short description (max 256 chars)

This is mean to be a realistic implementation of a shopping cart and order management system written in typescript and targeted to Next.js. The backend and infrastructure run in docker locally and will eventually run on Vercel and AWS.

Long Description

Temporal Commerce Demo — Project Description

A full-stack 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 15 · Temporal TypeScript SDK · Apache Cassandra · Elasticsearch
Scale: 111 source files · ~15,400 LOC · 6 Temporal workflow domains · 2,689 products


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.

Architecture

graph TB
    subgraph browser["Browser"]
        ui["React Storefront + Admin Panel"]
    end

    subgraph nextjs["Next.js Server"]
        actions["Server Actions<br/>(cart-actions.ts, admin-order-actions.ts)"]
        api["API Routes<br/>(/api/search, /api/product, /api/dev)"]
    end

    subgraph temporal["Temporal Server (6 Task Queues)"]
        cart["Cart Workflow<br/>cart-queue"]
        checkout["Checkout Workflow<br/>checkout-queue"]
        oms["Order Workflow<br/>oms-queue"]
        fulfillment["Fulfillment Workflow<br/>fulfillment-queue"]
        inventory["Inventory Service<br/>inventory-queue"]
        identity["Identity Workflows<br/>identity-queue"]
    end

    ui -->|"fetch / form submit"| actions
    ui -->|"GET"| api
    actions -->|"gRPC: updateWithStart, query, signal"| temporal
    temporal -->|"Activities"| cassandra[("Cassandra<br/>Write Store")]
    temporal -->|"Activities"| elasticsearch[("Elasticsearch<br/>Read Projections")]
    api -->|"Search / Read"| elasticsearch
Loading

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 Pattern

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

Pattern Implementation
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 — Multi-Step State Machine

Checkout orchestrates the shipping → payment → review → processing → complete lifecycle. Each step is advanced by a Temporal update with guard validation.

Pattern Implementation
Step guards Each update validates the current step before proceeding — shipping can be set from shipping, payment, or review (enabling back-navigation)
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 Execution

The fulfillment workflow receives pre-decided supplier orders and executes the appropriate fulfillment strategy for each.

Pattern Implementation
Automatic mode wf.sleep() timers simulate processing (15s) → shipping (15s) → delivery (15s)
Manual mode Feature flag MANUAL_FULFILLMENT=true pauses at each stage, waiting for Temporal signals to advance
Multi-supplier Strategy routing by supplierType — simulated, Printify dynamic, or custom
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 — User and Store Management

The identity domain manages user accounts, shoppers, API tokens, and store configuration through Temporal workflows.


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, inventory_reservations_w Inventory state
users, shoppers, api_tokens Identity management

Read Side — Elasticsearch

Elasticsearch serves as the read projection layer with full-text search and faceted filtering:

Index Projected By Consumer
products Reindex API (bulk) Storefront search, product detail
collections Reindex API (bulk) Collection navigation
orders OMS workflow activities Admin order list
supplier_orders OMS workflow activities Admin order detail

CQRS Flow

sequenceDiagram
    participant UI as Browser
    participant SA as Server Action
    participant WF as Temporal Workflow
    participant C as Cassandra
    participant ES as Elasticsearch

    UI->>SA: Add to Cart
    SA->>WF: updateWithStart (gRPC)
    WF->>WF: Update state in memory
    WF->>C: Persist via Activity
    WF->>ES: Project via Activity
    WF-->>SA: Return updated cart
    SA-->>UI: Render new state
Loading

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.

block-beta
    columns 3
    block:worker["Unified Worker Process (worker.ts)"]:3
        columns 3
        cart["Cart Worker\ncart-queue"]
        checkout["Checkout Worker\ncheckout-queue"]
        oms["OMS Worker\noms-queue"]
        fulfillment["Fulfillment Worker\nfulfillment-queue"]
        inventory["Inventory Worker\ninventory-queue"]
        identity["Identity Worker\nidentity-queue"]
    end
    space:3
    conn["Shared NativeConnection (single gRPC)"]:3

    cart --> conn
    checkout --> conn
    oms --> conn
    fulfillment --> conn
    inventory --> conn
    identity --> conn
Loading

Task queue isolation means a slow fulfillment activity cannot block cart operations. Each domain processes work independently even though they share a connection. In production, these can be split into separate deployments for independent scaling.


Key Temporal Patterns Demonstrated

# Pattern Where Used
1 updateWithStart — atomic lazy entity creation Cart
2 Query/Update handlers — workflow as live entity Cart, Checkout, OMS
3 continueAsNew — infinite entity lifetime Cart, Inventory Service
4 Parent-child with ABANDON policy Cart → Checkout
5 Step-based state machine with update guards 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 Multi-supplier strategy routing Fulfillment
10 Signal-driven status propagation Fulfillment → OMS → Elasticsearch
11 Workflow as CQRS event processor Inventory Service
12 Shared connection, isolated task queues Unified Worker
13 Dirty-flag projection batching OMS, Fulfillment

Error Handling — Redemptive State Recovery

The application follows a principle of 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 the payment step with an error message
  • 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

Project Structure

temporal-commerce-demo/
├── cassandra/                  # CQL schema definitions
├── sample-data/                # Product catalog (2,689 products, 40 collections)
├── scripts/                    # Seed orchestrator
├── docs/
│   ├── project-description.md  # This document
│   ├── presentation-script.md  # 30-40 min talk script with code excerpts
│   ├── demo-instructions.md    # 4-5 min live demo walkthrough
│   ├── developer-guide.md      # Local development setup
│   └── cloud-deployment.md     # Production deployment guide
├── src/
│   ├── app/
│   │   ├── api/                # REST endpoints (search, product, seed, reindex)
│   │   ├── admin/              # Admin panel (order management)
│   │   └── shop/               # Storefront (catalog, product, checkout)
│   ├── components/             # Shared UI (NavBar, CartDrawer, CheckoutProgress)
│   ├── context/                # React context (CartProvider)
│   ├── lib/                    # Shared clients (Cassandra, ES, Temporal)
│   └── temporal/
│       ├── contracts/          # Shared type definitions and constants
│       ├── cart/               # Cart workflow domain
│       ├── checkout/           # Checkout workflow domain
│       ├── oms/                # Order management domain
│       ├── fulfillment/        # Fulfillment simulation domain
│       ├── inventory/          # CQRS inventory domain
│       ├── identity/           # User and store management domain
│       └── worker.ts           # Unified worker launcher
└── docker-compose.yml          # Local infrastructure

Quick Start

npm install          # Install dependencies
make init            # Start Docker infrastructure + Cassandra schema
make app-start       # Start Next.js + Temporal workers
make seed            # Populate 2,689 products across 40 collections
Resource URL
Storefront http://localhost:3000/shop
Admin Panel http://localhost:3000/admin
Temporal UI http://localhost:8233

Technology Stack

Layer Technology Purpose
Frontend Next.js 15 (App Router), React 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; compatible with Temporal Cloud + managed databases

Related Documentation

Author(s)

Jeff Romine/Night Heron Software

Metadata

Metadata

Assignees

Labels

code exchange submissionCode and/or content about Temporal!triageIssues that Temporal folk need to look atziggy reviewedPre-screened by ZiggyBot

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions