Functional Core, Imperative Shell
These four words are like a cheat code for AI-assisted software development.
This article is part of the "move fast and brace things" series.
Want a cheat code to build serious applications with agentic AI that don't fall apart as they scale in complexity?
CHEAT CODE: Add these nine words to your project's CLAUDE.md file:
This project follows the functional core, imperative shell pattern.
What is the "Functional Core, Imperative Shell" Pattern?
Here's a good definition from functional-architecture.org:
Functional core, imperative shell is a pattern that structures software into two basic parts:
• The part that only depends on the inputs to produce the desired output – the pure functions – and
• the part that handles the interactions with the outside world – impure functions that handle the stateful infrastructure.
Further down the page from the definition above, there is a standalone section titled "When to reach for this pattern." They don't mince words:
It is always recommended to reach for this pattern as it always improves separation of concerns, testability, and maintainability.
Now, you may argue that the site I'm referencing is a site dedicated to "Functional Software Architecture." And given that this is the site's home page, you'd be on strong ground:

Now, you might further argue that, because the site is dedicated to functional software architecture, this pattern doesn't apply to VBA (which is typically a mix of imperative and object-oriented programming). The ground you'd be standing on for that argument, though, is more like...
But I digress.
Pure Functions
FCIS is a powerful pattern for software development because it maximizes the percentage of pure functions in your application. In fact, the goal of the pattern is to make the functional core as large as possible while keeping the imperative shell as thin as possible.
Why does that matter? Well, we should probably revisit the concept of pure functions first.
A pure function is a deterministic method with no side effects.
Being deterministic means that a given set of inputs will always produce the same outputs, making them easy to test.
And having no side effects means that you can safely call a pure function without having to worry about the effect it might have on the rest of your application.

The Trouble With Side Effects
You may recall from my earlier writing that side effects breed complexity, which manifests itself as long-term technical debt:

The problem with side effects is that they force you to keep extra things in your head. When a procedure creates a side effect or depends on some external state, you have to understand the full ramifications of that. Loading all that information into your head is like the late-show guest who spins 20 plates at a time.
You load the first part into your head. Then you load the next part which depends on the first part. And then a third part that depends on the second. You do that a few more times, and then you have to go back to the first part to remind yourself how the whole chain works.
It's the same as the spinning plate guy who spins three or four plates before having to go back to the first one to keep it spinning before loading the next plates.

The AI Context Window
AI has a similar problem with its own fancy name: the context window.
AI's short-term memory has very strict boundaries. Modern models have increased the size of that window (e.g., Opus 5 from Anthropic supports a 1-million-token context window), but there is still benefit in not filling it any more than necessary.
Pure functions are great because the AI can easily fit all the logic of the procedure into a minimal slice of the context window.
Test-Driven Development
The other key advantage with pure functions is that they are trivially easy to test.
You don't need to populate a database. You don't need to create test files in special folders. You don't need to set up fake email-sending endpoints to test messaging.
And because you don't need those things, the tests themselves are blazingly fast.
If Pure Functions are so Great, Why Have Anything Else?
What are you, a Haskell programmer?
Indeed, pure functions are so great that entire programming languages (more than one!) have been built around the concept. And yet, intentional side effects are a big part of the reason we write business apps in the first place. Purely functional languages make this necessary part of the process needlessly difficult.
The functional core, imperative shell pattern is the compromise that pairs the power of pure functions with the practicality of imperative code.
Thin Candy Shell
The goal of the pattern, though, is to maximize the functional core while minimizing the imperative shell.
Need to write a text file? Build the contents in a pure function that returns a string and pass that string to an imperative function whose only job is to write that string to a file. Even if the file is too big to take that approach, you can probably still create a pure function that generates the file one line at a time and passes those strings to an imperative procedure whose only job is to append to the file one line at a time. You get the idea.
An M&M represents the perfect ratio of chocolatey functional core to thin candy imperative shell.
[Tommy spills M&M's all over the dashboard]
Richard: Oh, that sounds good. Melted chocolate inside the dash, that really ups the resale value.
Tommy: I think you'll be okay here, they have a thin candy shell. Surprised you didn't know that.
Further Reading
For more information, check out Code that Fits in Your Head, by Mark Seemann.

Many of the concepts I will be discussing in this AI series come from that book.
*All text in this article written by a 100%-certified free-range human.



