Temporal turns a multi-step business process into a durable function: workflow state survives crashes and restarts because Temporal replays history instead of asking you to persist it by hand. That one property is worth more than the rest of the feature list combined once you've been the person paged because a "workflow engine" built on a cron job and a status column silently stalled a batch of orders halfway through.
Durable execution: what actually survives a crash
The pitch for Temporal is easy to state and easy to underrate: a workflow function keeps running logically even if the process executing it dies mid-step. This isn't magic — Temporal doesn't keep your process alive. What it does is record every meaningful thing that happens inside a workflow (each activity call, each timer, each signal received) as an event in a history log owned by the Temporal server. When a worker picks the workflow back up — because it crashed, redeployed, or the pod got rescheduled — it doesn't resume from a snapshot. It replays the workflow function from the top, and the Temporal SDK feeds it the recorded results of everything that already happened instead of re-executing it. Deterministic code plus recorded history equals a function that behaves as if it had been running continuously the entire time, even across a two-week gap waiting on a customer to approve a quote.
This is the part people find unintuitive: your workflow code has to be deterministic, because it gets re-run from scratch on every replay. Anything non-deterministic — reading the current time, generating a random ID, calling an external API directly — has to go through the SDK's workflow APIs (workflow.now(), side-effect helpers) or, more commonly, get pushed into an activity. Activities are where non-determinism and side effects live; workflows are where control flow lives. Get that split wrong and replay produces a different result than the original run, and Temporal will tell you loudly.
Workflow-as-code vs. a hand-rolled state machine
Before Temporal, the usual way to build something like order-to-cash was a status column on an orders table plus a cron job or a queue consumer that moved rows from one status to the next: pending to paid to fulfilled to invoiced. It works until the process has more than four or five steps, or until two steps need to happen concurrently, or until you need to wait an indeterminate amount of time for a human to click approve. At that point the status column grows extra columns to track sub-state, the cron job grows extra branches to handle each transition, and the actual business logic — the thing a new hire should be able to read in one sitting — is smeared across a scheduler, a queue consumer, and a handful of conditionals that only make sense if you already know the shape of the process.
Temporal's pitch is that you write the process as a single function, top to bottom, in a real programming language, and the durability comes from the runtime rather than from you modeling state transitions by hand. A multi-step approval chain is a for loop that waits on a signal per approver. A billing cycle is a loop with a sleep. The code reads like the process because it is the process — there's no separate state machine diagram that the code has to stay in sync with, because the workflow function is the state machine.
func OrderToCashWorkflow(ctx workflow.Context, order Order) (Receipt, error) {
ao := workflow.ActivityOptions{
StartToCloseTimeout: 30 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: time.Second,
BackoffCoefficient: 2.0,
MaximumInterval: time.Minute,
MaximumAttempts: 5,
},
}
ctx = workflow.WithActivityOptions(ctx, ao)
var reservation Reservation
if err := workflow.ExecuteActivity(ctx, ReserveInventory, order).Get(ctx, &reservation); err != nil {
return Receipt{}, err
}
var charge ChargeResult
if err := workflow.ExecuteActivity(ctx, ChargeCard, order.Payment).Get(ctx, &charge); err != nil {
// Compensation: release what we already reserved before giving up.
_ = workflow.ExecuteActivity(ctx, ReleaseInventory, reservation).Get(ctx, nil)
return Receipt{}, fmt.Errorf("payment failed, inventory released: %w", err)
}
var shipment Shipment
if err := workflow.ExecuteActivity(ctx, ScheduleShipment, order, reservation).Get(ctx, &shipment); err != nil {
// Two prior steps succeeded — unwind both in reverse order.
_ = workflow.ExecuteActivity(ctx, RefundCharge, charge).Get(ctx, nil)
_ = workflow.ExecuteActivity(ctx, ReleaseInventory, reservation).Get(ctx, nil)
return Receipt{}, fmt.Errorf("shipment scheduling failed, charge refunded: %w", err)
}
var receipt Receipt
err := workflow.ExecuteActivity(ctx, EmitInvoice, order, charge, shipment).Get(ctx, &receipt)
return receipt, err
}Every workflow.ExecuteActivity call in that function is a durable checkpoint. If the worker process dies right after ChargeCard succeeds but before ScheduleShipment starts, nothing is lost — Temporal's history already recorded that the charge activity completed, and on replay the workflow resumes exactly at the shipment step without re-charging the customer.
Retries and backoff stop being your problem
Notice that none of the activity calls above have a manual retry loop around them. That's the other half of what you're buying: every activity gets Temporal's retry policy applied automatically, with exponential backoff, a configurable maximum interval, and a cap on attempts, all declared once as data rather than re-implemented as a try/catch/sleep loop at every call site. If the payment gateway returns a 503, the activity fails, Temporal schedules a retry after the backoff interval, and your workflow code doesn't need to know the retry happened — it just sees an activity that eventually returned or eventually exhausted its attempts. You can also mark specific errors as non-retryable (a card decline should not retry five times; a gateway timeout should) so the retry policy respects the difference between "transient" and "final" failures.
Temporal guarantees a workflow's control flow replays correctly, but it can't guarantee an activity only has one real-world effect if it's retried mid-flight — a charge activity that succeeds on the provider's side but times out before your worker sees the response will be retried by Temporal into what looks, from the outside, like a duplicate charge. Give side-effecting activities an idempotency key (an order ID, a request UUID) and check for it on the receiving end. The retry policy handles the "when to try again" problem; you still own the "what happens if I try the same thing twice" problem.
Compensation and the saga pattern
The code above is a saga: a sequence of steps where each one either completes or triggers a rollback of everything that came before it. Temporal doesn't ship a saga framework baked into the SDK, but it makes hand-writing one tractable, because the compensation logic is just more workflow code with the same durability guarantees as the forward path. In the order-to-cash example, if shipment scheduling fails after the charge and the inventory reservation both succeeded, the workflow runs RefundCharge and ReleaseInventory as ordinary activities — each with its own retry policy, each individually durable, each recorded in the same history. If the worker crashes during the compensation itself, replay picks the rollback back up exactly where it left off, the same way it would for the forward path.
This is the detail that's easy to miss coming from a queue-based system: compensating actions are not a separate, less-reliable code path bolted on afterward. They're activities like any other, which means a refund that fails transiently gets retried with backoff just like a charge would, and a refund that fails permanently surfaces as an error you can alert on instead of a silently abandoned rollback.
A saga assumes rollback steps succeed, but "refund the card" can fail for the same reasons "charge the card" can. Give compensating activities their own bounded retry policy and an explicit terminal state — page a human, write to a reconciliation queue, whatever fits — rather than letting a failed compensation just return an error that ends the workflow with money or inventory in an inconsistent state nobody's watching.
Where this is worth the operational cost
Temporal is not free to run — it's a server (or Temporal Cloud), a database backing it, and workers you deploy and version like any other service, plus a real learning curve around determinism and versioning long-running workflows through code changes. For a process that finishes in one HTTP request, none of this pays for itself; a normal function call is the correct answer. It earns its cost when a process spans real wall-clock time with points where it waits on something outside your control — a human approval that might take three days, a subscription billing cycle that runs monthly for the life of the account, an order fulfillment chain touching three or four external systems where any one of them can be slow or down. Those are exactly the processes that used to live in cron jobs and status columns, quietly accumulating edge cases nobody wants to touch. Temporal doesn't remove the complexity of the business process itself — it removes the accidental complexity of making that process survive the reality of servers restarting, deploys happening mid-flight, and the network being unreliable.
Wrapping up
The core trade you're making with Temporal is writing your business process as ordinary, deterministic code and letting the runtime own durability, retries, and timing — instead of writing that same process as a status machine plus a scheduler plus a pile of manual retry logic and hoping you've covered every place a crash could leave things half-done. For anything long-running with real steps that can each independently fail — order-to-cash, approval chains, billing cycles — that trade is worth it. For a request that completes in milliseconds, it's a tool you don't need yet.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.