Unit 05.04: Keeping side effects out of retries
The general fix for retry safety is to split every node into the part that can run freely and the part that must not.
Pure nodes and guarded nodes
A pure node computes and returns; running it five times changes nothing but time. An impure node touches the outside world, and needs a flag in the state that it checks before acting.
The code runs three cycles of prepare-then-charge.
from typing import TypedDict
class State(TypedDict):
validated: bool
charged: bool
receipt: str
attempts: int
def prepare(state: State):
"""Pure: safe to run any number of times."""
return {"validated": True, "attempts": state["attempts"] + 1}
def charge(state: State):
"""Impure: guarded so a retry cannot double-charge."""
if state["charged"]:
return {"receipt": state["receipt"]}
return {"charged": True, "receipt": "rcpt-001"}
s = {"validated": False, "charged": False, "receipt": "", "attempts": 0}
for cycle in (1, 2, 3):
s = {**s, **prepare(s)}
s = {**s, **charge(s)}
print(f"cycle {cycle}: attempts={s['attempts']} charged={s['charged']} receipt={s['receipt']}")
print("\nthree cycles, one charge")
# Split every node into pure and impure. Pure nodes can sit inside a retry loop
# freely. Impure ones need a guard flag in the state, and that flag has to be
# written before the effect, not after -- or a crash mid-call retries it.
Three cycles, one charge. prepare runs every time and that is fine - it only recomputes. charge reads charged and returns early.
The ordering inside charge matters more than it appears. The flag must be written as part of the same update as the effect; if the effect happens and the process dies before the state is checkpointed, the retry sees charged: False and charges again. Where the external system supports it, the idempotency key from the first unit is the stronger guarantee, because it survives your process disappearing entirely.
The mistake this prevents
The mistake is putting a side effect in a node that also does computation. The node then cannot be retried safely even though most of it could be, so either you skip the retry and lose reliability, or you retry and duplicate the effect. Split it.
Takeaway
Separate pure computation from outward effects. Pure nodes retry freely; impure ones need both a state flag and, where possible, an idempotency key at the far end.
