Determinism
Context
At the architectural level, one of the most valuable properties a piece of software can have is predictability: given the same inputs and the same state, it produces the same outputs every time. This property is called determinism. It’s the foundation of testing, debugging, and reasoning about what a program does.
Determinism sounds obvious. Of course a computer should give the same answer twice. But in practice, it’s surprisingly easy to lose. Random number generators, system clocks, network calls, file system state, thread scheduling, and floating-point rounding can all introduce variation between runs. In agentic coding, the AI agent itself is often nondeterministic: the same prompt can produce different code on different runs.
Problem
You write a function, test it, and it works. You run it again with the same inputs, and it gives a different answer, or works on your machine but fails on another. How do you build reliable software when the same operation can produce different results depending on invisible factors?
Forces
- Repeatability vs. real-world interaction: Pure computation can be deterministic, but interacting with the outside world (networks, clocks, users) inherently introduces variation.
- Testability vs. flexibility: Deterministic functions are easy to test, but many useful operations (generating unique IDs, fetching current data) are inherently nondeterministic.
- Debugging ease vs. performance: Capturing enough state to reproduce a run exactly may be expensive in time or storage.
- Agent predictability vs. creativity: Nondeterminism in AI agents enables creative solutions but makes results harder to verify.
Solution
Separate the deterministic core of your logic from the nondeterministic edges. Keep the parts of your system that make decisions and transform data as pure functions: functions that depend only on their inputs and produce only their return value, with no Side Effects. Push nondeterministic elements (current time, random values, external data) to the boundaries, and pass them into the deterministic core as explicit inputs.
This pattern is sometimes called “functional core, imperative shell.” The core is deterministic and testable. The shell handles the messy real world and feeds clean inputs to the core.
When working with AI agents, determinism takes on a specific flavor. Agent outputs are typically nondeterministic: you can’t guarantee the same prompt produces the same code. The practical response is to verify agent output through deterministic means. Run the tests, check the types, validate the behavior. You accept nondeterminism in the generation process but enforce determinism in the acceptance criteria.
How It Plays Out
A billing system calculates monthly charges. The calculation depends on usage data and rate tables, both of which can be made deterministic inputs. The developer structures the calculation as a pure function: given these usage records and these rates, the charge is exactly this amount. The function that fetches usage data from the database is separate and nondeterministic, but the billing logic itself can be tested with fixed inputs and expected outputs, confidently and repeatedly.
When you ask an AI agent to generate a function, check whether it introduces hidden nondeterminism: calls to the current time, random values, or external services embedded inside what should be pure logic. Ask the agent to extract those dependencies as parameters instead.
A team notices that their integration tests pass locally but fail intermittently on the build server. Investigation reveals that two tests depend on the order in which they run; one test leaves data behind that the other consumes. The tests are nondeterministic because they depend on shared mutable state. Fixing the tests means making each one self-contained: set up its own state, run, and clean up.
“Extract the billing calculation into a pure function that takes usage records and rate tables as parameters and returns the charge amount. Move the database fetch and the current-time call outside this function.”
Consequences
Deterministic systems are dramatically easier to test, debug, and reason about. When a bug is reported, you can reproduce it by supplying the same inputs. When a test fails, you know it’ll fail again the same way, making diagnosis straightforward.
The cost is that strict determinism requires discipline in how you structure code: separating pure logic from side effects, making dependencies explicit, and sometimes sacrificing a small amount of convenience. It also means accepting that some parts of the system (user input, network responses, AI agent output) will never be deterministic, and building your verification strategy around that reality.
Related Patterns
- Refined by: Side Effect — eliminating side effects is the primary technique for achieving determinism.
- Enables: Algorithm — deterministic behavior is what makes algorithms reliably testable.
- Contrasts with: Concurrency — concurrent execution introduces nondeterminism through scheduling, even in otherwise deterministic code.
- Contrasts with: Event — event-driven systems often process events in nondeterministic order.
- Used by: API — consumers expect API calls with the same inputs to produce predictable results.
- Contrasts with: Protocol — protocols must account for nondeterministic factors like network latency and partial failure.