WoGu

Why determinism bugs are hard to catch in code review

By WoGu Team

UUID.randomUUID() is not a bug. Thread.sleep(5000) is not a bug. Math.random() is not a bug. Each of these is completely ordinary code, and a reviewer skimming a pull request has no reason to slow down on any of them — until the class calling it happens to be a Temporal workflow implementation, or happens to be reachable from one.

The bug is in the call graph, not the call site

Consider a workflow that calls a service method, which calls another service method, which happens to read System.currentTimeMillis(). Nothing at the workflow's own call site looks suspicious — the non-determinism is two hops away, inside a class that might not even have "workflow" in its name. A reviewer would have to trace that call graph by hand, every time, for every one of a dozen non-deterministic APIs, to catch it. In practice, nobody does this consistently, and the ones who try do it inconsistently.

This is exactly why WG001 through WG010 are built on a call-graph analyzer rather than a simple "grep the workflow class" check: the bug WoGu is designed to catch is specifically the one hidden behind a helper method.

It fails at the worst possible time

A NonDeterministicException doesn't happen when the code runs the first time — it happens on replay: after a worker restart, a deployment, or a handoff to a different worker. That means the bug can sit latent through code review, through a successful deploy, and through days of normal operation, only to surface the next time a worker needs to reconstruct history. By then, the person debugging it is rarely the person who wrote the offending line, and the stack trace points at Temporal's replay machinery, not at the three-hop call that actually caused it.

Catching it at build time changes the economics

A rule that runs on every mvn verify catches the violation in the same feedback loop as a failing unit test — before merge, with the exact call path from the workflow's entry point down to the offending call rendered in the report. That's the same shift that made build-time coverage and bug-pattern gates useful: moving the check from "something a careful reviewer might catch" to "something the build always catches."

See Call Graph Analysis for how the traversal actually works, and where it deliberately stops.