{"article":{"slug":"deterministic-core-non-deterministic-shell","title":"Deterministic Core, Non-Deterministic Shell","subtitle":null,"summary":"Fourteen years after Functional Core, Imperative Shell, Outdata argues for a deterministic core with a non-deterministic shell—keeping pure logic testable while isolating AI and I/O uncertainty.","content_type":"essay","language":"en","canonical_url":"https://outdata.net/blog/260803","author":{"name":"Outdata","url":"https://outdata.net","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Outdata","url":"https://outdata.net","listing_slug":null,"listing":null},"topics":[{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"AI Agents","slug":"ai-agents","url":"https://listedarticles.com/topics/ai-agents"},{"name":"Software Architecture","slug":"software-architecture","url":"https://listedarticles.com/topics/software-architecture"},{"name":"LLMs","slug":"llms","url":"https://listedarticles.com/topics/llms"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":1004,"reading_minutes":4,"published_at":"2026-08-03T00:00:00.000Z","added_at":"2026-09-21T03:09:19.817Z","updated_at":"2026-09-21T03:09:19.817Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/deterministic-core-non-deterministic-shell","markdown_url":"https://listedarticles.com/articles/deterministic-core-non-deterministic-shell.md","example":false,"citation":"Outdata, Outdata. \"Deterministic Core, Non-Deterministic Shell.\" 3 Aug 2026. https://outdata.net/blog/260803 (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://outdata.net/blog/260803"},"body_markdown":"# Deterministic Core, Non-Deterministic Shell\n\n3 Aug 2026\n\nFourteen years ago, Gary Bernhardt coined the term\n[Functional Core, Imperative Shell](https://www.youtube.com/watch?v=yTkzNHF6rMs). Like most good ideas in computing it was not entirely new, but his\nconception had great clarity, and it forms an excellent basis for talking\nabout testing and determinism in existing systems.\n\nBriefly, Functional Core/Imperative Shell architecture divides the code into\ntwo parts. The Functional Core is purely functional - that is no IO, and no\ndestructive state updates. It is concerned with the business logic of the\napplication. The Imperative Shell has comparatively little pathing, but\nmaintains state, coordinates external dependencies, and deals with the outside\nworld - that is to say IO. Its job is to query the core with values, receive\nvalues back as the result of some blackbox decision, and use that to interact\nwith the outside world; whether that's writing to a database, sending a\nrequest, or updating a GUI.\n\nThe Shell and the Core in this model have distinct characteristics:\n\nCore\nShell\n\nMakes decisions\nCoordinates dependencies\n\nMany branching execution paths\nMore linear execution\n\nIsolated from the world\nIntegrates with the world\n\nThis makes the core very amenable to testing. Since it's purely functional,\nthe same inputs will always get the same results. Since it's isolated, there\nis nothing to mock or stub. And since it handles complex business logic, the\ntests can tell us a lot about how the system behaves.\n\n## Functional Purity and Determinism\n\nA shorter way of describing the properties that make pure functions amenable\nto testing is that they are deterministic. That is - given a stream of\ninputs, a pure function always returns the same stream of outputs; their\nbehaviour is repeatable. But pure functional programming is not the only way\nto get there. If we tilt our heads a little we can see that a stream of values\nand a sequence of assignments are [different ways of expressing the same thing](https://www.iro.umontreal.ca/~feeley/cours/ift6232/doc/a-correspondence-between-algol-60-and-churchs-lambda-notation.pdf), and State Machines can bring\nus the same benefits. Consider the following code:\n\nfunction add(ns) {\nreturn ns.reduce((a, b) => a + b, 0)\n}\n\nclass AddMachine {\n#state = 0\n\ntransition(input) {\nthis.#state += input\n}\n\nget state() {\nreturn this.#state\n}\n}\n\nThe function add is easy to reason about; it's pure and thus\ndeterministic. But the AddMachine is also deterministic - given\nthe same sequence of calls to the transition function, AddMachine\nwill return the same state. It being imperative does not change that.\n\nconst output = add([1, 2, 3]) // 6\n\nconst a = new AddMachine()\na.transition(1)\na.transition(2)\na.transition(3)\n\nconst output = a.state // 6\n\nPure functional programming is a fine paradigm, but due to language or\nperformance considerations, it is not always practical - I would not want to\ntry it in C! But weakening the requirements from purely functional to\nmerely deterministic, we retain the testability benefits of \"Functional\nCore, Imperative Shell\", while broadening its applicability. And so the title\nof this post:\nDeterministic Core, Non-Deterministic Shell.\n\nDeterminism can feel like a more abstract concept than functional purity. How\ndo you know it when you see it? I find it's easier to start with what is not\ndeterministic and work backwards. Here are some common examples of\nnon-repeatable behaviour:\n\n- Calling RNGs that aren't seeded\n\n- Asynchronous and multi-threaded operations\n\n- Communication over the network\n\n- Communication with other processes\n\n- Reading/Writing to local storage\n\n- Database interactions\n\n- Asking the OS for the date or time\n\nAll these belong in the non-deterministic shell. Whenever you find them in\nyour business logic, you have a natural target for defragmentation - either\nsplitting the function in two around them, or lifting them up a layer and\ninjecting their result as a parameter. It's illustrative to think of the\n\"shell\" metaphor quite literally; it should surround the logic, querying the\nheart of the application to get what it needs.\n\n## Working with what you have\n\n\"This is all well and good\", you might think, \"but what use of it is to me,\ntoiling away in the legacy & vibe-code mines of industry?\". A fair accusation,\nimaginary reader; [not everyone can be Foundation DB](https://www.youtube.com/watch?v=4fFDFbi3toc&t=966s&pp=ygUsd2lsbCB3aWxzb24gZGV0ZXJtaW5pc3RpYyBzaW11bGF0aW9uIHRlc3Rpbmc%3D) and make that distinction from day one\n(they actually went a step further, but that's a topic for another post).\nDeterminism and non-determinism are highly entwined in almost every real life\ncodebase I have seen, and I've seen my fair share.\n\nBut don't let perfect be the enemy of good! One way to think of your average\n(ie, terrible) codebase is that it has many deterministic cores. There are\nthousands, strewn through the slop as stars in the sky. The glass half empty\ntake is these codebases are an irredeemable legacy mess. But glass half full\nis that there are many deterministic cores hidden somewhere inside, and maybe\nonly a handful.\n\nUsers of older Windows systems may remember the \"Disk Defragmenter\"; it took\nfiles whose contents were scattered physically across the spinning hard disk\nand made them contiguous. In an era where read speed depended on physical\ndistance on the media, this mattered a lot.\n\nThere was something so satisfying about seeing the red segments slowly give\nway to the blue.\n\nSo one gradual approach for existing code is to practice the Defragmentation\nof Determinism. Identify it wherever you can - files, classes, even a few\nlines in individual functions - and start collecting them. The more\ndeterminism that can be grouped, the more easily testable functionality you\nhave, and the more you can feel confident about the behaviour and reliability\nof the program as a whole. The surface area for \"hard to test\"\n(non-deterministic code) starts to shrink. On a large enough codebase you will\nlikely never get to a single deterministic core, but even hundreds is better\nthan thousands.\n\n## Unleash the State Machine Within!\n\nEvery nasty mess of a codebase I've seen has one or more much nicer\ndeterministic state machines locked inside. I promise you they are there, even\nif it's not obvious. And once you find them, you'll be delighted with how much\neasier the software is to modify and test. Piece by piece, reliability can be\nwrought.\n\n- [Share on Hacker News](https://news.ycombinator.com/submitlink?u=https://outdata.net/blog/260803&t=Deterministic Core, Non-Deterministic Shell)\n\n- [Share on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://outdata.net/blog/260803)","body_html":"<h1 id=\"deterministic-core-non-deterministic-shell\">Deterministic Core, Non-Deterministic Shell</h1>\n<p>3 Aug 2026</p>\n<p>Fourteen years ago, Gary Bernhardt coined the term\n<a href=\"https://www.youtube.com/watch?v=yTkzNHF6rMs\" rel=\"nofollow ugc noopener\">Functional Core, Imperative Shell</a>. Like most good ideas in computing it was not entirely new, but his\nconception had great clarity, and it forms an excellent basis for talking\nabout testing and determinism in existing systems.</p>\n<p>Briefly, Functional Core/Imperative Shell architecture divides the code into\ntwo parts. The Functional Core is purely functional - that is no IO, and no\ndestructive state updates. It is concerned with the business logic of the\napplication. The Imperative Shell has comparatively little pathing, but\nmaintains state, coordinates external dependencies, and deals with the outside\nworld - that is to say IO. Its job is to query the core with values, receive\nvalues back as the result of some blackbox decision, and use that to interact\nwith the outside world; whether that&#39;s writing to a database, sending a\nrequest, or updating a GUI.</p>\n<p>The Shell and the Core in this model have distinct characteristics:</p>\n<p>Core\nShell</p>\n<p>Makes decisions\nCoordinates dependencies</p>\n<p>Many branching execution paths\nMore linear execution</p>\n<p>Isolated from the world\nIntegrates with the world</p>\n<p>This makes the core very amenable to testing. Since it&#39;s purely functional,\nthe same inputs will always get the same results. Since it&#39;s isolated, there\nis nothing to mock or stub. And since it handles complex business logic, the\ntests can tell us a lot about how the system behaves.</p>\n<h2 id=\"functional-purity-and-determinism\">Functional Purity and Determinism</h2>\n<p>A shorter way of describing the properties that make pure functions amenable\nto testing is that they are deterministic. That is - given a stream of\ninputs, a pure function always returns the same stream of outputs; their\nbehaviour is repeatable. But pure functional programming is not the only way\nto get there. If we tilt our heads a little we can see that a stream of values\nand a sequence of assignments are <a href=\"https://www.iro.umontreal.ca/~feeley/cours/ift6232/doc/a-correspondence-between-algol-60-and-churchs-lambda-notation.pdf\" rel=\"nofollow ugc noopener\">different ways of expressing the same thing</a>, and State Machines can bring\nus the same benefits. Consider the following code:</p>\n<p>function add(ns) {\nreturn ns.reduce((a, b) =&gt; a + b, 0)\n}</p>\n<p>class AddMachine {\n#state = 0</p>\n<p>transition(input) {\nthis.#state += input\n}</p>\n<p>get state() {\nreturn this.#state\n}\n}</p>\n<p>The function add is easy to reason about; it&#39;s pure and thus\ndeterministic. But the AddMachine is also deterministic - given\nthe same sequence of calls to the transition function, AddMachine\nwill return the same state. It being imperative does not change that.</p>\n<p>const output = add([1, 2, 3]) // 6</p>\n<p>const a = new AddMachine()\na.transition(1)\na.transition(2)\na.transition(3)</p>\n<p>const output = a.state // 6</p>\n<p>Pure functional programming is a fine paradigm, but due to language or\nperformance considerations, it is not always practical - I would not want to\ntry it in C! But weakening the requirements from purely functional to\nmerely deterministic, we retain the testability benefits of &quot;Functional\nCore, Imperative Shell&quot;, while broadening its applicability. And so the title\nof this post:\nDeterministic Core, Non-Deterministic Shell.</p>\n<p>Determinism can feel like a more abstract concept than functional purity. How\ndo you know it when you see it? I find it&#39;s easier to start with what is not\ndeterministic and work backwards. Here are some common examples of\nnon-repeatable behaviour:</p>\n<ul><li>Calling RNGs that aren&#39;t seeded</li><li>Asynchronous and multi-threaded operations</li><li>Communication over the network</li><li>Communication with other processes</li><li>Reading/Writing to local storage</li><li>Database interactions</li><li>Asking the OS for the date or time</li></ul>\n<p>All these belong in the non-deterministic shell. Whenever you find them in\nyour business logic, you have a natural target for defragmentation - either\nsplitting the function in two around them, or lifting them up a layer and\ninjecting their result as a parameter. It&#39;s illustrative to think of the\n&quot;shell&quot; metaphor quite literally; it should surround the logic, querying the\nheart of the application to get what it needs.</p>\n<h2 id=\"working-with-what-you-have\">Working with what you have</h2>\n<p>&quot;This is all well and good&quot;, you might think, &quot;but what use of it is to me,\ntoiling away in the legacy &amp; vibe-code mines of industry?&quot;. A fair accusation,\nimaginary reader; <a href=\"https://www.youtube.com/watch?v=4fFDFbi3toc&amp;t=966s&amp;pp=ygUsd2lsbCB3aWxzb24gZGV0ZXJtaW5pc3RpYyBzaW11bGF0aW9uIHRlc3Rpbmc%3D\" rel=\"nofollow ugc noopener\">not everyone can be Foundation DB</a> and make that distinction from day one\n(they actually went a step further, but that&#39;s a topic for another post).\nDeterminism and non-determinism are highly entwined in almost every real life\ncodebase I have seen, and I&#39;ve seen my fair share.</p>\n<p>But don&#39;t let perfect be the enemy of good! One way to think of your average\n(ie, terrible) codebase is that it has many deterministic cores. There are\nthousands, strewn through the slop as stars in the sky. The glass half empty\ntake is these codebases are an irredeemable legacy mess. But glass half full\nis that there are many deterministic cores hidden somewhere inside, and maybe\nonly a handful.</p>\n<p>Users of older Windows systems may remember the &quot;Disk Defragmenter&quot;; it took\nfiles whose contents were scattered physically across the spinning hard disk\nand made them contiguous. In an era where read speed depended on physical\ndistance on the media, this mattered a lot.</p>\n<p>There was something so satisfying about seeing the red segments slowly give\nway to the blue.</p>\n<p>So one gradual approach for existing code is to practice the Defragmentation\nof Determinism. Identify it wherever you can - files, classes, even a few\nlines in individual functions - and start collecting them. The more\ndeterminism that can be grouped, the more easily testable functionality you\nhave, and the more you can feel confident about the behaviour and reliability\nof the program as a whole. The surface area for &quot;hard to test&quot;\n(non-deterministic code) starts to shrink. On a large enough codebase you will\nlikely never get to a single deterministic core, but even hundreds is better\nthan thousands.</p>\n<h2 id=\"unleash-the-state-machine-within\">Unleash the State Machine Within!</h2>\n<p>Every nasty mess of a codebase I&#39;ve seen has one or more much nicer\ndeterministic state machines locked inside. I promise you they are there, even\nif it&#39;s not obvious. And once you find them, you&#39;ll be delighted with how much\neasier the software is to modify and test. Piece by piece, reliability can be\nwrought.</p>\n<ul><li>Share on Hacker News</li><li><a href=\"https://www.linkedin.com/sharing/share-offsite/?url=https://outdata.net/blog/260803\" rel=\"nofollow ugc noopener\">Share on LinkedIn</a></li></ul>","headings":[{"level":1,"text":"Deterministic Core, Non-Deterministic Shell","id":"deterministic-core-non-deterministic-shell"},{"level":2,"text":"Functional Purity and Determinism","id":"functional-purity-and-determinism"},{"level":2,"text":"Working with what you have","id":"working-with-what-you-have"},{"level":2,"text":"Unleash the State Machine Within!","id":"unleash-the-state-machine-within"}]}}