Where a New Programming Model Comes From, and What It Costs

· stealthrocket.tech ·

6 min read Original article ↗

Every programming model that has ever mattered started as somebody’s boilerplate.

Before garbage collection there was a great deal of code whose only job was remembering to release memory, and a great many bugs from getting it wrong. Before async/await there was a shape of callback nesting so recognisable it got a name. Before durable execution there were hand-rolled state machines, a status column, and a cron job that looked for rows stuck in IN_PROGRESS.

In each case the sequence was the same, and it is worth setting out plainly, because it predicts what the next one will feel like.

The sequence

One. A problem is solved repeatedly, by hand, in application code. Nobody calls it a pattern yet; it is just what you have to do.

Two. Someone names the pattern. This is the step that changes things, because a named thing can be criticised. “Callback hell” was not a technical discovery, it was a rhetorical one, and it made the cost visible to people who had stopped noticing it.

Three. A library appears that encapsulates the pattern. Promises. A retry decorator. A WorkflowRunner class with a resume() method. The library is genuinely useful and genuinely insufficient, because the pattern is about control flow and a library cannot change control flow.

Four. The runtime or the language absorbs it, and the ergonomics change out of recognition: because now the compiler and the scheduler are in on it. await is not a function; it is a suspension point the runtime knows about. That is the whole difference.

Five. The restriction shows up. This is the step everyone forgets when they are excited about step four. Garbage collection took away deterministic destruction, which is why C++ programmers were not unanimously delighted and why IDisposable, try-with-resources, defer and context managers all exist. async took away the ability to call a function from anywhere, which is what “function colouring” describes. Replay-based durability takes away nondeterminism: clocks, random numbers, iteration order, ambient I/O.

The restriction is not the price of a bad design. It is the mechanism. A runtime can only give you a guarantee about code it can reason about, and it can only reason about code that agreed to be reasoned about.

What durable execution is doing in this sequence

It is at step four, and has been for a few years, which is why the conversation around it is currently so noisy.

The pattern being absorbed is: a long process, made of steps, that must survive the loss of the machine running it. The hand-rolled version is a table with a status column. The library version is a job framework with checkpoints you call yourself. The runtime version (Temporal, Restate, DBOS, Step Functions) makes the checkpoint the engine’s job, and the smaller runtimes doing coroutine migration make the checkpoint implicit, so that the code reads like a plain function and the log happens underneath.

And the restriction is arriving on schedule. Determinism in workflow code. Serialisable arguments. No ambient I/O in the replayed body. A hard split between what can be retried and what merely computes. Every team adopting this is currently having the argument that C++ teams had about destructors and that JavaScript teams had about coloured functions, and mostly without realising it is the same argument.

A tangent about defer

Go’s defer is a small and instructive artefact of exactly this. Go has garbage collection, so it took away deterministic memory cleanup, but files, locks and connections still need deterministic release. defer is the patch: a language feature that exists purely to restore something an earlier language feature removed. Python’s with, C#’s using and Rust’s decision to keep destructors entirely are three other answers to the same gap.

Worth noticing because it suggests where the durable-execution patch will land. Something will need to restore controlled access to the things the replay body is not allowed to touch — the clock, randomness, the outside world — in a form the runtime can still account for. workflow.now(), workflow.uuid4() and side-effect wrappers are early, awkward versions of that patch. They are not elegant yet. Neither was defer when it was goto cleanup.

The adoption cost, stated honestly

Three costs, in the order teams hit them.

The first is the rewrite, and it is the one people plan for. Splitting a process into steps the runtime can name is real work and it is roughly proportional to how much I/O the process does.

The second is the tooling gap, and it is the one people underestimate. A new programming model invalidates debuggers, profilers, log correlation and, most painfully, the mental model everyone on the team uses to read a stack trace. A stack trace from a replayed function is not a history of what happened; it is a history of the current replay. Nobody’s instincts are calibrated for that on day one.

The third is the hiring and review cost, which nobody plans for at all. Reviewing code under a new model requires knowing the restriction well enough to spot a violation, and violations of determinism are invisible in a diff. sorted(items) is fine. for x in items_set is a latent bug. No linter catches all of it and no reviewer catches it reliably in the first year.

Which is not an argument against adopting one

The models won for a reason. Manual memory management produced a decade of exploitable bugs. Callback nesting produced code nobody could modify. Hand-rolled state machines produce exactly the half-finished payment run that motivated the whole category.

But the honest framing is a trade with a named cost rather than an upgrade. You are exchanging a class of bug you keep hitting for a constraint you will have to live inside, plus a year of tooling being slightly wrong. That trade is very often correct. It is never free, and a runtime that markets it as free is telling you something about the runtime and nothing about the model.

The question worth asking before adopting any of them is narrow and answerable: which specific failure has hurt this system in the last twelve months? If the answer is “a process died halfway and nobody knew what had already happened”, the trade is probably worth it. If the answer is something else, the boilerplate you are about to abolish was not costing you very much.