<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <link rel="self" href="https://outdata.net/blog/feed.xml" type="application/atom+xml"/>
  <title>Outdata Blog</title>
  <subtitle>Software Reliability &amp; Testing</subtitle>
  <link href="https://outdata.net/"/>
  <updated>2026-08-03T04:48:49.400Z</updated>
  <id>https://outdata.net/</id>
  <author><name>Lewis Campbell</name></author>
  
    <entry>
      <title>Deterministic Core, Non-Deterministic Shell</title>
      <link href="https://outdata.net/blog/260803"/>
      <id>https://outdata.net/blog/260803</id>
      <updated>2026-08-02T12:00:00.000Z</updated>
      <summary>Determinism, architecture, and the de-sloppification of legacy code.</summary>
      <content type="html">

&lt;p&gt;
  Fourteen years ago, Gary Bernhardt coined the term
  &lt;a href=&quot;https://www.youtube.com/watch?v=yTkzNHF6rMs&quot;&gt;
    Functional Core, Imperative Shell
  &lt;/a&gt;. Like most good ideas in computing it was not entirely new, but his
  conception had great clarity, and it forms an excellent basis for talking
  about testing and determinism in existing systems.
&lt;/p&gt;
&lt;p&gt;
  Briefly, Functional Core/Imperative Shell architecture divides the code into
  two parts. The Functional Core is purely functional - that is no IO, and no
  destructive state updates. It is concerned with the business logic of the
  application. The Imperative Shell has comparatively little pathing, but
  maintains state, coordinates external dependencies, and deals with the outside
  world - that is to say IO. Its job is to query the core with values, receive
  values back as the result of some blackbox decision, and use that to interact
  with the outside world; whether that&apos;s writing to a database, sending a
  request, or updating a GUI.
&lt;/p&gt;

&lt;p&gt;The Shell and the Core in this model have distinct characteristics:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th scope=&quot;col&quot;&gt;Core&lt;/th&gt;
      &lt;th scope=&quot;col&quot;&gt;Shell&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Makes decisions&lt;/td&gt;
      &lt;td&gt;Coordinates dependencies&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Many branching execution paths&lt;/td&gt;
      &lt;td&gt;More linear execution&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Isolated from the world&lt;/td&gt;
      &lt;td&gt;Integrates with the world&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;
  This makes the core very amenable to testing. Since it&apos;s purely functional,
  the same inputs will always get the same results. Since it&apos;s isolated, there
  is nothing to mock or stub. And since it handles complex business logic, the
  tests can tell us a lot about how the system behaves.
&lt;/p&gt;
&lt;h2&gt;Functional Purity and Determinism&lt;/h2&gt;
&lt;p&gt;
  A shorter way of describing the properties that make pure functions amenable
  to testing is that they are &lt;i&gt;deterministic&lt;/i&gt;. That is - given a stream of
  inputs, a pure function always returns the same stream of outputs; their
  behaviour is repeatable. But pure functional programming is not the only way
  to get there. If we tilt our heads a little we can see that a stream of values
  and a sequence of assignments are &lt;a
    href=&quot;https://www.iro.umontreal.ca/~feeley/cours/ift6232/doc/a-correspondence-between-algol-60-and-churchs-lambda-notation.pdf&quot;
  &gt;different ways of expressing the same thing&lt;/a&gt;, and State Machines can bring
  us the same benefits. Consider the following code:
&lt;/p&gt;
&lt;pre&gt;
function add(ns) {
 return ns.reduce((a, b) =&gt; a + b, 0)
}

class AddMachine {
  #state = 0

  transition(input) {
    this.#state += input
  }

  get state() {
    return this.#state
  }
}
&lt;/pre&gt;
&lt;p&gt;
  The function &lt;code&gt;add&lt;/code&gt; is easy to reason about; it&apos;s pure and thus
  deterministic. But the &lt;code&gt;AddMachine&lt;/code&gt; is also deterministic - given
  the same sequence of calls to the transition function, &lt;code&gt;AddMachine&lt;/code&gt;
  will return the same state. It being imperative does not change that.
&lt;/p&gt;
&lt;pre&gt;
const output = add([1, 2, 3]) // 6
&lt;/pre&gt;

&lt;pre&gt;
const a = new AddMachine()
a.transition(1)
a.transition(2)
a.transition(3)

const output = a.state // 6
&lt;/pre&gt;

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

&lt;p&gt;
  Determinism can feel like a more abstract concept than functional purity. How
  do you know it when you see it? I find it&apos;s easier to start with what is not
  deterministic and work backwards. Here are some common examples of
  non-repeatable behaviour:
&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;
    Calling RNGs that aren&apos;t seeded
  &lt;/li&gt;
  &lt;li&gt;
    Asynchronous and multi-threaded operations
  &lt;/li&gt;
  &lt;li&gt;
    Communication over the network
  &lt;/li&gt;
  &lt;li&gt;
    Communication with other processes
  &lt;/li&gt;
  &lt;li&gt;
    Reading/Writing to local storage
  &lt;/li&gt;
  &lt;li&gt;
    Database interactions
  &lt;/li&gt;
  &lt;li&gt;
    Asking the OS for the date or time
  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;
  All these belong in the non-deterministic shell. Whenever you find them in
  your business logic, you have a natural target for defragmentation - either
  splitting the function in two around them, or lifting them up a layer and
  injecting their result as a parameter. It&apos;s illustrative to think of the
  &quot;shell&quot; metaphor quite literally; it should surround the logic, querying the
  heart of the application to get what it needs.
&lt;/p&gt;

&lt;h2&gt;Working with what you have&lt;/h2&gt;

&lt;p&gt;
  &quot;This is all well and good&quot;, you might think, &quot;but what use of it is to me,
  toiling away in the legacy &amp; vibe-code mines of industry?&quot;. A fair accusation,
  imaginary reader; &lt;a
    href=&quot;https://www.youtube.com/watch?v=4fFDFbi3toc&amp;t=966s&amp;pp=ygUsd2lsbCB3aWxzb24gZGV0ZXJtaW5pc3RpYyBzaW11bGF0aW9uIHRlc3Rpbmc%3D&quot;
  &gt;not everyone can be Foundation DB&lt;/a&gt; and make that distinction from day one
  (they actually went a step further, but that&apos;s a topic for another post).
  Determinism and non-determinism are highly entwined in almost every real life
  codebase I have seen, and I&apos;ve seen my fair share.
&lt;/p&gt;

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

&lt;p&gt;
  Users of older Windows systems may remember the &quot;Disk Defragmenter&quot;; it took
  files whose contents were scattered physically across the spinning hard disk
  and made them contiguous. In an era where read speed depended on physical
  distance on the media, this mattered a lot.
&lt;/p&gt;

&lt;figure&gt;
  &lt;img
    src=&quot;/img/windows-xp-disk-defragmenter.png&quot;
    alt=&quot;Disk Defragmenter tool in Windows XP&quot;
  &gt;
  &lt;figcaption&gt;
    There was something so satisfying about seeing the red segments slowly give
    way to the blue.
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;
  So one gradual approach for existing code is to practice the Defragmentation
  of Determinism. Identify it wherever you can - files, classes, even a few
  lines in individual functions - and start collecting them. The more
  determinism that can be grouped, the more easily testable functionality you
  have, and the more you can feel confident about the behaviour and reliability
  of the program as a whole. The surface area for &quot;hard to test&quot;
  (non-deterministic code) starts to shrink. On a large enough codebase you will
  likely never get to a single deterministic core, but even hundreds is better
  than thousands.
&lt;/p&gt;

&lt;h2&gt;Unleash the State Machine Within!&lt;/h2&gt;

&lt;p&gt;
  Every nasty mess of a codebase I&apos;ve seen has one or more much nicer
  deterministic state machines locked inside. I promise you they are there, even
  if it&apos;s not obvious. And once you find them, you&apos;ll be delighted with how much
  easier the software is to modify and test. Piece by piece, reliability can be
  wrought.
&lt;/p&gt;
</content>
    </entry>
  
</feed>