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
| Layer | Responsibility | Example |
|---|---|---|
| PURISTA | Business logic, type safety, message routing | createOrder, processPayment, reserveInventory |
| Temporal | Workflow 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 case | PURISTA alone | PURISTA + Temporal |
|---|---|---|
| Simple CRUD | ✅ | Overkill |
| Event-driven reactions | ✅ | Overkill |
| 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