Persist and Rehydrate
A Plexis domain lives in memory. In a serverless or edge runtime, the process does not persist between requests — each invocation starts fresh. If your domain models a flow that spans multiple requests (a checkout flow, an approval workflow, a multi-step form), you need to serialize the domain after each event and reconstruct it before the next one.
This guide shows the full round-trip: snapshot → serialize → store → read → deserialize → restore.
The scenario: an order checkout spread across requests
Section titled “The scenario: an order checkout spread across requests”Each HTTP request handles one user action. The domain is created fresh per request, restored from the database, an event is followed, and the updated snapshot is written back.
import { defineDomain, when, on, terminal } from '@tde.io/plexis';import type { DomainSnapshot } from '@tde.io/plexis';
// ── 1. Define the domain ─────────────────────────────────────────────────────
type CheckoutCtx = { orderId: string; items: string[]; shippingAddress: string | null; paymentMethod: string | null;};
function createCheckoutDomain() { return defineDomain<CheckoutCtx>('checkout', () => { when('cart', () => { on('setShipping', target('address')); }); when('address', () => { on('setPayment', target('payment')); on('back', target('cart')); }); when('payment', () => { on('confirm', target('confirmed')); on('back', target('address')); }); when('confirmed', terminal());
return { context: { orderId: '', items: [], shippingAddress: null, paymentMethod: null, }, initial: 'cart', }; });}2. Serialize and store a snapshot
Section titled “2. Serialize and store a snapshot”After each event, call domain.snapshot() to get a plain { phase, context, historyLength } object. It is JSON-safe as long as the context contains only JSON-serializable values.
// Simulated database client — replace with your actual storeconst db = { async set(key: string, value: string) { /* write to KV/DB */ }, async get(key: string): Promise<string | null> { return null; },};
async function saveCheckout(orderId: string, snap: DomainSnapshot<CheckoutCtx>) { await db.set(`checkout:${orderId}`, JSON.stringify(snap));}3. Load and restore a snapshot
Section titled “3. Load and restore a snapshot”On a later request, read the serialized snapshot and call domain.restore() to put the domain back in the saved state.
async function loadCheckout(orderId: string): Promise<DomainSnapshot<CheckoutCtx> | null> { const raw = await db.get(`checkout:${orderId}`); if (!raw) return null; return JSON.parse(raw) as DomainSnapshot<CheckoutCtx>;}4. Request handler — full round-trip
Section titled “4. Request handler — full round-trip”Each request: create a fresh domain, restore if a prior snapshot exists, follow the event, save the updated snapshot.
type Request = { orderId: string; event: string; payload?: unknown };type Response = { state: string; context: CheckoutCtx };
async function handleCheckoutRequest(req: Request): Promise<Response> { const domain = createCheckoutDomain();
// Restore from the last saved snapshot (or start fresh if none) const saved = await loadCheckout(req.orderId); if (saved) { domain.restore(saved); } else { // First request — seed orderId into context domain.restore({ state: 'cart', context: { orderId: req.orderId, items: [], shippingAddress: null, paymentMethod: null }, historyLength: 0, }); }
// Follow the event from the request const result = await domain.follow(req.event as any, req.payload);
if (result.status !== 'followed') { throw new Error(`Transition ${req.event} was ${result.status}`); }
// Persist the updated snapshot await saveCheckout(req.orderId, domain.snapshot());
return { phase: domain.phase, context: domain.context };}5. Example request sequence
Section titled “5. Example request sequence”// Request 1: Start the checkout (domain starts in 'cart', no prior snapshot)await handleCheckoutRequest({ orderId: 'ord_1', event: 'setShipping' });// domain state: 'address', saved to DB
// Request 2: On a different function instance — domain is reconstructed from DBawait handleCheckoutRequest({ orderId: 'ord_1', event: 'setPayment' });// domain restored to 'address', transitions to 'payment', saved to DB
// Request 3: Confirm the orderawait handleCheckoutRequest({ orderId: 'ord_1', event: 'confirm' });// domain restored to 'payment', transitions to 'confirmed', saved to DBEach request creates a new domain instance. The domain object never crosses request boundaries — only the serialized snapshot does.
What is and is not in the snapshot
Section titled “What is and is not in the snapshot”DomainSnapshot contains:
state— the current state name (a string).context— a copy of the context object. Must be JSON-serializable.historyLength— the count of completed transitions.
Not in the snapshot: handler functions (enter, exit, action, guard), active subscribers, and full history entries. These are part of the definition or the live runtime, not the serialized state.
Version skew
Section titled “Version skew”If the domain definition changes between the time a snapshot was saved and the time it is restored, the restoration may fail or produce unexpected behavior:
- A renamed state will cause
restore()to throwPlexisErrorif the stored state name no longer exists. - Added context fields will be absent in old snapshots (provide defaults in
createCheckoutDomainif needed). - Removed context fields will silently remain in restored context.
Version your snapshots and write migration functions for breaking definition changes. See Snapshots and Restore in Patterns for the mechanics.
Using a domain factory for clean per-request construction
Section titled “Using a domain factory for clean per-request construction”The createCheckoutDomain factory pattern (from this guide) is described in detail in Reusable Factories. It keeps the definition clean and makes per-request construction trivial.