Deterministic Core, Non-Deterministic Shell

LAC-Tech2 pts0 comments

Deterministic Core, Non-Deterministic Shell

Deterministic Core, Non-Deterministic Shell

3 Aug 2026

Fourteen years ago, Gary Bernhardt coined the term

Functional Core, Imperative Shell<br>. Like most good ideas in computing it was not entirely new, but his<br>conception had great clarity, and it forms an excellent basis for talking<br>about testing and determinism in existing systems.

Briefly, Functional Core/Imperative Shell architecture divides the code into<br>two parts. The Functional Core is purely functional - that is no IO, and no<br>destructive state updates. It is concerned with the business logic of the<br>application. The Imperative Shell has comparatively little pathing, but<br>maintains state, coordinates external dependencies, and deals with the outside<br>world - that is to say IO. Its job is to query the core with values, receive<br>values back as the result of some blackbox decision, and use that to interact<br>with the outside world; whether that's writing to a database, sending a<br>request, or updating a GUI.

The Shell and the Core in this model have distinct characteristics:

Core<br>Shell

Makes decisions<br>Coordinates dependencies

Many branching execution paths<br>More linear execution

Isolated from the world<br>Integrates with the world

This makes the core very amenable to testing. Since it's purely functional,<br>the same inputs will always get the same results. Since it's isolated, there<br>is nothing to mock or stub. And since it handles complex business logic, the<br>tests can tell us a lot about how the system behaves.

Functional Purity and Determinism

A shorter way of describing the properties that make pure functions amenable<br>to testing is that they are deterministic. That is - given a stream of<br>inputs, a pure function always returns the same stream of outputs; their<br>behaviour is repeatable. But pure functional programming is not the only way<br>to get there. If we tilt our heads a little we can see that a stream of values<br>and a sequence of assignments are different ways of expressing the same thing, and State Machines can bring<br>us the same benefits. Consider the following code:

function add(ns) {<br>return ns.reduce((a, b) => a + b, 0)

class AddMachine {<br>#state = 0

transition(input) {<br>this.#state += input

get state() {<br>return this.#state

The function add is easy to reason about; it's pure and thus<br>deterministic. But the AddMachine is also deterministic - given<br>the same sequence of calls to the transition function, AddMachine<br>will return the same state. It being imperative does not change that.

const output = add([1, 2, 3]) // 6

const a = new AddMachine()<br>a.transition(1)<br>a.transition(2)<br>a.transition(3)

const output = a.state // 6

Pure functional programming is a fine paradigm, but due to language or<br>performance considerations, it is not always practical - I would not want to<br>try it in C! But weakening the requirements from purely functional to<br>merely deterministic, we retain the testability benefits of "Functional<br>Core, Imperative Shell", while broadening its applicability. And so the title<br>of this post:<br>Deterministic Core, Non-Deterministic Shell .

Determinism can feel like a more abstract concept than functional purity. How<br>do you know it when you see it? I find it's easier to start with what is not<br>deterministic and work backwards. Here are some common examples of<br>non-repeatable behaviour:

Calling RNGs that aren't seeded

Asynchronous and multi-threaded operations

Communication over the network

Communication with other processes

Reading/Writing to local storage

Database interactions

Asking the OS for the date or time

All these belong in the non-deterministic shell. Whenever you find them in<br>your business logic, you have a natural target for defragmentation - either<br>splitting the function in two around them, or lifting them up a layer and<br>injecting their result as a parameter. It's illustrative to think of the<br>"shell" metaphor quite literally; it should surround the logic, querying the<br>heart of the application to get what it needs.

Working with what you have

"This is all well and good", you might think, "but what use of it is to me,<br>toiling away in the legacy & vibe-code mines of industry?". A fair accusation,<br>imaginary reader; not everyone can be Foundation DB and make that distinction from day one<br>(they actually went a step further, but that's a topic for another post).<br>Determinism and non-determinism are highly entwined in almost every real life<br>codebase I have seen, and I've seen my fair share.

But don't let perfect be the enemy of good! One way to think of your average<br>(ie, terrible) codebase is that it has many deterministic cores. There are<br>thousands, strewn through the slop as stars in the sky. The glass half empty<br>take is these codebases are an irredeemable legacy mess. But glass half full<br>is that there are many deterministic cores hidden somewhere inside, and maybe<br>only a handful.

Users of older Windows systems may remember the "Disk Defragmenter"; it took<br>files whose contents were...

deterministic core shell functional state imperative

Related Articles