Skip to content
About
Main content

Durable execution

How does logging every effect let one program survive crashes, and when is it better than a DAG?

This is this mechanism's real alternative architecture, not a feature. It is worth deriving from the beginning, because it reverses the starting assumption.

Two ways to externalize state

The DAG model externalizes state by decomposing the work. Each task is a separate process, and the state between tasks lives in storage the does not read.

Durable execution keeps the work as one program and externalizes state by logging effects. Every call to an external system (an activity) has its result appended to a durable log. When the process dies, a new process the function from the start. Every logged call returns its recorded result instead of running again. The program arrives at exactly the position it had, with its local variables, loops and conditions intact.

def refund_workflow(request):             # runs as ordinary code
    ok   = activity(validate, request)    # result is logged the first time
    appr = wait_for_signal("approved")    # durable wait: a row, not a process
    ref  = activity(issue_refund, request)
    try:
        activity(notify_customer, request)
    except Failure:
        activity(reverse_refund, ref)     # compensation is just another activity
# After a crash: replay from the top. validate/issue_refund return logged results
# without running again; execution continues at the first activity not yet in the log.

This is redo recovery (replaying a log to rebuild state after a crash) applied to program control flow instead of database pages. A appears three times in this mechanism: the , the run history and the activity log. It is the clearest sign that durability has exactly one implementation.

The strict constraint: replay must be deterministic

If the function branches on now(), a random value or a direct network call, the replay takes a different path than the original, and the logged results no longer match the calls being made. : time, randomness, I/O and concurrency. It routes each of them through the log. This is the same requirement as the ban on now() in task bodies (Safe re-execution), but enforced by the runtime instead of by convention. That difference is the main thing you buy.

When to use which

DAG of tasks
State between stepsIn external storage; the orchestrator cannot see itIn the workflow's own variables, logged
Unit of retryThe task (coarse, chosen by the author)The activity (fine, natural)
Control flowStatic graph; loops and conditions are awkwardOrdinary code
Step granularityMinutes to hours; launch cost forbids smallerMilliseconds; no process per step
Natural workloadBulk data transformationLong-lived business processes, sagas, approvals
VersioningSilent semantic drift (Safe re-execution)Explicit divergence that must be handled in code
VisibilityThe graph can be inspected before runningThe shape is known only by running

A is a long business transaction made of steps that each have a (an action that undoes the effect: refund reverses a charge). If a later step fails, the earlier steps are compensated. Durable execution makes sagas practical, because compensations are just more activities in the log.

The clean dividing line:

  • Durable execution is better where state between steps is small and control flow is complicated: order fulfillment, multi-step provisioning, sagas, anything with a human in it.
  • The DAG model is better where state between steps is enormous and control flow is trivial. That is most data engineering: the state is a hundred-gigabyte table that cannot live in the workflow's variables, and the graph is a straight line.

Neither replaces the other. They solve problems that look similar only because both draw boxes and arrows.

Log every effect and replay the program to recover its position — a WAL for control flow, which buys fine-grained retry and real branching at the cost of enforced determinism, and which is right when the inter-step state is small.