Enterprise Patterns

Temporal Orchestration

Complex workflow orchestration with Temporal

PURISTA excels at building isolated, message-driven business capabilities. Temporal excels at orchestrating long-running, failure-prone processes across those capabilities. Together, they handle the full spectrum from simple commands to complex sagas.

What each layer does

LayerResponsibilityExample
PURISTABusiness logic, type safety, message routingcreateOrder, processPayment, reserveInventory
TemporalWorkflow orchestration, retries, timeouts, compensation”Reserve inventory, then charge payment, then ship — and undo if anything fails”
sequenceDiagram
    participant T as Temporal Workflow
    participant P as PURISTA Event Bridge
    participant S1 as Inventory Service
    participant S2 as Payment Service
    participant S3 as Shipping Service

    T->>P: command: reserveInventory
    P->>S1: reserve items
    S1->>P: success: inventoryReserved
    P->>T: response

    T->>P: command: processPayment
    P->>S2: charge card
    S2->>P: success: paymentProcessed
    P->>T: response

    T->>P: command: createShipment
    P->>S3: schedule delivery
    S3->>P: success: shipmentCreated
    P->>T: response

Why combine them

PURISTA handles immediate request/response and event-driven reactions well. But some business processes span minutes, hours, or days:

  • Order fulfillment — reserve stock, charge payment, ship, confirm delivery
  • User onboarding — send email, wait for verification, provision account
  • Billing cycles — generate invoices, retry failed charges, escalate
  • Approval workflows — submit request, wait for human approval, execute or reject

Temporal manages the orchestration layer:

  • Retries with backoff — retry failed activities automatically
  • Timeouts — fail or compensate when steps take too long
  • Sagas — undo completed steps when later steps fail
  • Human-in-the-loop — pause workflows until external signals arrive
  • Observability — see the full workflow state, history, and pending actions

Architecture

flowchart TB
    subgraph TEMPORAL["Temporal"]
        W["Workflow Engine"]
        A["Activities"]
    end
    subgraph PURISTA["PURISTA"]
        EB["Event Bridge"]
        S1["Order Service"]
        S2["Payment Service"]
        S3["Email Service"]
    end
    W -->|calls| A
    A -->|command messages| EB
    EB --> S1
    EB --> S2
    EB --> S3
    S1 -->|events| EB
    S2 -->|events| EB
    S3 -->|events| EB
    EB -->|signals| W

Temporal activities send PURISTA command messages. PURISTA subscriptions can signal Temporal workflows when events occur.

When to use Temporal

Use casePURISTA alonePURISTA + Temporal
Simple CRUDOverkill
Event-driven reactionsOverkill
Multi-step process with retries⚠️
Human approval required
Saga/compensation pattern
Long-running (hours/days)

Integration pattern

Temporal activity → PURISTA command

Activities are plain async functions registered with the Temporal worker. Each activity uses the PURISTA event bridge client to invoke a PURISTA command:

// activities.ts — registered with the Temporal Worker
// puristaClient is your event bridge or HTTP client that invokes PURISTA commands
// (e.g., an AmqpBridge or NatsBridge instance, or an HTTP client talking to a PURISTA HTTP endpoint)

export async function reserveInventory(orderId: string): Promise<void> {
  await puristaClient.invoke('InventoryService', '1', 'reserveInventory', { orderId })
}

export async function processPayment(orderId: string): Promise<void> {
  await puristaClient.invoke('PaymentService', '1', 'processPayment', { orderId })
}

export async function createShipment(orderId: string): Promise<void> {
  await puristaClient.invoke('ShippingService', '1', 'createShipment', { orderId })
}
// workflow.ts — Temporal workflow definition
import { proxyActivities } from '@temporalio/workflow'

const { reserveInventory, processPayment, createShipment } = proxyActivities<
  typeof import('./activities')
>({ startToCloseTimeout: '30s' })

export async function orderWorkflow(orderId: string): Promise<void> {
  await reserveInventory(orderId)
  await processPayment(orderId)
  await createShipment(orderId)
}
// worker.ts — start the Temporal worker
import { Worker } from '@temporalio/worker'
import * as activities from './activities.js'

const worker = await Worker.create({
  workflowsPath: require.resolve('./workflow'),
  activities,
  taskQueue: 'order-fulfillment',
})
await worker.run()

PURISTA subscription → Temporal signal

A PURISTA subscription can signal a running Temporal workflow using @temporalio/client:

import { Client as TemporalClient } from '@temporalio/client'

const temporalClient = new TemporalClient()

const paymentProcessedSub = notificationServiceBuilder
  .getSubscriptionBuilder('onPaymentProcessed', 'Signal Temporal')
  .subscribeToEvent('paymentProcessed')
  .setSubscriptionFunction(async function (context, payload) {
    const handle = temporalClient.workflow.getHandle(payload.orderId)
    await handle.signal('paymentCompleted', { orderId: payload.orderId })
    return { status: 'ack' }
  })

Common pitfalls

  • Using Temporal for simple flows. PURISTA queues and events handle most workflows.
  • Tight coupling between Temporal and PURISTA. Use events and commands as the boundary.
  • Ignoring idempotency. Temporal retries activities. Commands must be idempotent.
  • Not handling signals. Temporal workflows may need signals from PURISTA events.

Checklist

  • Workflow is genuinely long-running or complex
  • Temporal activities invoke idempotent PURISTA commands
  • PURISTA events signal Temporal workflows where needed
  • Compensation logic is defined for saga patterns
  • Workflow state is observable in Temporal UI
  • Timeouts and retries are configured per activity

Related

Read Next
Observability

from Observability & Operations