Skip to content
Go back

Durable Execution for Dummies: Retries Protect Calls, Workflows Protect Processes

Published:  at  08:00 PM

Retries are not durable execution

Retries are not durable execution.

That sounds obvious when written down. But in real architecture conversations the distinction gets blurry very quickly.

Someone says:

We already have retries.
We use exponential backoff.
We have circuit breakers.
We use Polly, Resilience4j, Tenacity, Spring Retry, Opossum, or whatever the equivalent is in our stack.

Good. You should.

But those tools usually sit around a call.

Durable execution sits around a process.

That is the abstraction shift.

The difference is the abstraction layer

Resilience depends on what you wrap

A retry library wraps an operation:

It asks a small, local question:

Did this operation succeed?

If not, maybe retry it. Maybe back off. Maybe open a circuit. Maybe timeout. Maybe fall back.

That is valuable. It protects a running process from transient dependency failure.

Durable execution wraps something larger:

It asks a different question:

Can this process safely continue?

That is not just a bigger retry. It is a different abstraction layer.

The easy mistake

Imagine this order flow:

Receive order
Authorize payment
Reserve stock
Create invoice
Send confirmation
Notify fulfilment

Interactive workflow simulator

Retries protect calls. Workflows protect processes.

Durable execution persists workflow history outside the worker, so crashes and long outages can resume.

Successful runs
0
Transient failures
0
Recovered failures
0
Complete failures
0

Higher values make dependency errors, restarts, and long outages more likely.

Time in seconds the simulator pauses between each workflow step.

Step 1running

Receive order

Validate the incoming command and assign a workflow id.

0%
Step 2idle

Authorize payment

Call the payment gateway with an idempotency key.

0%
Step 3idle

Reserve stock

Hold inventory before making downstream promises.

0%
Step 4idle

Create invoice

Persist the commercial record after prerequisites pass.

0%
Step 5idle

Send confirmation

Notify the customer once the order is safe to confirm.

0%
Step 6idle

Notify fulfillment

Hand the completed order to the operational system.

0%

Run 1 started. Receive order is running.

A resilience library can help with the call to the invoice service.

If the invoice API returns 503, retry it. If it times out, back off. If it keeps failing, open the circuit. Great.

But now ask a different question:

What happens if the worker crashes after the payment is authorized but before the invoice is created?

A retry policy around the invoice call does not answer that.

Neither does a circuit breaker.

Neither does a timeout.

The problem is no longer “did this HTTP call succeed?” The problem is:

Where exactly was this business process, what already happened, and what is safe to do next?

That is durable execution territory.

A reliable call is not the same as a recoverable process

Call failure versus process failure

This is the distinction I wish more architecture diagrams made visible.

Call-level resilience protects a single dependency interaction. Process-level durability protects the journey across interactions.

A reliable call can still live inside an unrecoverable process.

For example:

1. Charge payment gateway
2. Update CRM
3. Send confirmation email
4. Publish OrderConfirmed event

Each individual call might have a lovely retry policy. Each might use backoff. Each might have timeouts. Each might be observable.

But if all four side effects live inside one retryable unit, the recovery story is still messy.

If the process crashes after step 1, do you charge again?

If it crashes after step 2, do you update CRM again?

If the email send succeeds but the event publish fails, what is the source of truth?

If the whole activity retries, which side effects are safe to repeat?

This is why durable execution platforms such as Temporal, Azure Durable Functions, Dapr Workflow, AWS Step Functions, and Netflix Conductor exist.

They are not just retry engines. They are process recovery engines.

They persist progress outside the worker process. They keep history. They know which step ran. They know what is waiting. They can resume after crashes, restarts, redeployments, callbacks, timers, and human input.

The developer still has to design the steps well. But the platform gives the process somewhere to live other than memory.

The call-level question

Call-level resilience

Call-level resilience asks:

Did this operation succeed?

That is the world of:

In .NET, Polly is the obvious example. In Java you might think of Resilience4j or Spring Retry. In Python, Tenacity. In Node.js, Opossum.

These libraries are not the enemy. They are useful and necessary.

But they normally live inside a running process.

If the process is gone, the retry policy is gone with it.

The durable-execution question

Durable execution workflow

Durable execution asks:

Can this process safely continue?

That means the runtime needs to know things like:

This is why durable execution is so powerful for real business processes.

It is not about making every call magically succeed. Distributed systems still fail.

It is about making the process survivable.

The activity-design problem

Durable execution solves the process recovery problem.

It does not remove the need to think carefully about side effects.

This is the part people often miss.

A workflow engine can remember that an activity failed. It can retry it. It can show you where the workflow is stuck. It can resume after the worker comes back.

But if your activity performs four different external writes and then crashes halfway through, the workflow engine cannot magically know whether your external systems are in a safe state unless you design for that.

That is where my own shorthand comes in:

WOWL: Write Once, Write Last

WOWL: Write Once, Write Last

WOWL: Write once, write last activity design

WOWL is a simple rule of thumb for retryable steps:

Do repeatable work first. Put one externally visible write at the end.

Or shorter:

Read many. Decide once. Write last.

Inside a retryable activity, reads are usually fine:

Read order
Read customer
Read current price
Read account status
Validate
Calculate
Prepare request

If that activity fails before the final side effect, it can usually run again.

The danger begins when the activity changes the outside world:

Charge payment
Send email
Update CRM
Publish event
Create shipment
Write audit record

Those are not just calculations. They are side effects.

A side effect should either be:

The important thing is not the acronym. The important thing is the boundary.

Before the boundary: repeatable work.

After the boundary: one controlled side effect.

Bad smell: one activity sprays writes everywhere

This is the smell I look for in workflow code:

Activity: CompleteOrder

1. Charge payment gateway
2. Update CRM
3. Send confirmation email
4. Publish event
5. Update reporting database

This activity name sounds helpful. It is also hiding a recovery nightmare.

What happens if step 1 succeeds and step 2 fails?

What happens if step 3 succeeds but step 4 times out?

What happens if the activity retries from the beginning?

What happens if the payment gateway is idempotent but the email provider is not?

What happens if the event publish succeeds but the local database write does not?

You can make this work. But you have to design it. If you do not, you have created a process that looks simple in code and behaves chaotically under failure.

Better: split the flow into safe steps

Why WOWL matters: safe steps versus spray writes

A better workflow shape is often:

Activity 1: ChargePayment
Activity 2: RecordPaymentResult
Activity 3: UpdateCrm
Activity 4: SendConfirmationEmail
Activity 5: PublishOrderConfirmedEvent

Each activity has one meaningful side effect.

Each activity can have its own idempotency strategy.

Each activity has a clearer retry boundary.

The workflow then coordinates the process:

ChargePayment -> RecordPaymentResult -> UpdateCrm -> SendEmail -> PublishEvent

Now the durable execution platform can show where the business process is. And when something fails, the question is not “which of the five hidden writes happened inside this one blob of code?”

The question is much cleaner:

Which process step failed, and is that step safe to retry?

Important nuance: not all writes are equal

WOWL is a rule of thumb, not a law of physics.

Multiple database writes inside one real database transaction can be fine. If they commit atomically, they behave as one write boundary.

For example:

BEGIN TRANSACTION
  Insert order row
  Insert order line rows
  Insert audit row
COMMIT

That is not the same problem as this:

Charge payment gateway
Update CRM
Send email
Publish Kafka event

The first has one transactional boundary.

The second has four different external systems and no single atomic commit across all of them.

That is the distinction.

WOWL is mostly about externally visible side effects across system boundaries.

What about events?

Event publishing is a write.

This is easy to forget because events feel lightweight. They are “just messages”.

But if another service can observe it, react to it, bill from it, email from it, reserve stock from it, or update customer state from it, then it is a side effect.

That means event publishing needs the same discipline as any other write.

In many systems, the safest answer is the transactional outbox pattern:

  1. Write business state and an outgoing event record in the same database transaction.
  2. Have a separate publisher reliably publish pending events.
  3. Make consumers idempotent.

Again, the theme is the same: make the boundary explicit.

Durable execution does not replace idempotency

This is worth saying plainly.

Durable execution does not make side effects safe by itself.

It makes the process recoverable.

Your activities still need idempotency.

If you charge a credit card twice, the workflow engine cannot pretend that did not happen.

If you send the same email ten times, the workflow history may explain why, but the customer still received ten emails.

If you publish duplicate events and consumers are not idempotent, the blast radius moves downstream.

Durable execution gives you state, history, retries, timers, waits, signals, and recovery.

WOWL gives you a practical design discipline for the side effects inside that durable process.

They belong together.

How I think about the layers

You often need both: call-level resilience and durable execution

The cleanest mental model is this:

Durable workflow
  Activity: read/prepare -> one idempotent write
  Activity: read/prepare -> one idempotent write
  Activity: read/prepare -> one idempotent write

Inside each activity, call-level resilience still matters.

You still want timeouts.

You still want bounded retries.

You still want circuit breakers in the right places.

You still want rate limits.

You still want observability.

But those techniques protect individual operations.

The durable workflow protects the process.

A practical checklist

When reviewing workflow or activity code, I ask these questions:

1. What is the unit of retry?

If this thing fails, what exactly will run again?

A method? An activity? A message handler? A background job? A whole workflow step?

You cannot reason about side effects until you know the retry boundary.

2. Which operations are reads?

Reads are usually repeatable, but be careful with unstable reads. Reading “the current price” or “the current risk score” may be technically read-only but semantically time-sensitive.

Sometimes you need to snapshot the input so the process is deterministic from a business perspective.

3. Which operations are writes?

Be strict here.

A write is anything that changes externally observable state.

That includes sending email, publishing events, creating tickets, charging payment methods, starting shipments, mutating databases, updating CRM, calling webhooks, and sometimes even calling third-party APIs that record requests.

4. Can the write be repeated safely?

If yes, how?

If the answer is “probably”, that is not an answer.

5. Is the write last?

If the write is followed by more logic, ask why.

Sometimes there is a good reason. Often there is not.

The safest retryable shape is:

read -> read -> calculate -> validate -> prepare -> write

not:

write -> calculate -> write -> call -> write -> hope

The one-sentence version

Call resilience asks:

Did this operation succeed?

Durable execution asks:

Can this process safely continue?

WOWL asks:

If this step retries, what side effect might happen again?

That combination is the point.

Retries make calls more reliable.

Durable execution makes processes recoverable.

WOWL keeps the side-effect boundary clean.

Update: Powell’s Law

Working through the ideas in this article, and in related conversations about durable execution and workflow design, I arrived at a broader principle that sits above WOWL.

Powell’s Law: In any resilient system, interruptions should cost time, not progress.

WOWL is the activity-level design rule. It keeps retryable side effects safe by placing one controlled write at the end of a step, so that a retry does not silently repeat work that already happened.

Powell’s Law is the broader systems principle. After a crash, a restart, a redeployment, a dependency outage, a retry, a human hand-off, or any other interruption, the system should resume from the furthest correct durable point rather than unnecessarily repeat completed work.

Execution may be disposable. Progress should be durable.

The specific techniques that help satisfy the law include:

None of these techniques are new. What Powell’s Law tries to name is the common principle they all express: the system should never lose confirmed progress simply because the execution environment was interrupted. The only cost is time.


Suggest Changes
Share this post on:

Previous Post
Temporal Nexus in .NET (Preview): What It Is, When to Use It, and a Practical Walkthrough
Next Post
Return on Intelligence, Part 8: The New Power Map