Functional Core, Imperative Shell
An architecture pattern where all decisions live in pure functions at the center, and a thin outer layer handles the side effects: reading input, talking to the network, writing to disk.
Also known as: FCIS, Functional Core Imperative Shell, Imperative Shell, Functional Core
Category: Software Development
Tags: software-architecture, software-design, software-development, functional-programming, testing, architecture
Explanation
Most code mixes two very different kinds of work. Some of it decides things: calculating a price, validating an order, picking the next state. Some of it does things: reading files, calling APIs, writing to the database, sending emails. When both are tangled in the same functions, everything gets harder. Testing needs mocks. Bugs hide in the interaction. Reasoning about a function means knowing everything it touches.
Gary Bernhardt named the fix in 2012, in his Destroy All Software screencast "Functional Core, Imperative Shell", and expanded on it in his talk "Boundaries" at SCNA 2012. Split the program in two.
**The two parts**:
- **The functional core**: Pure functions working on plain values. No I/O, no clock, no randomness, no global state. Given the same input, they always return the same output. This is where the logic and the decisions live
- **The imperative shell**: A thin layer around the core. It gathers input from the outside world, calls the core, then performs the side effects the core asked for. It contains as few decisions as possible
The core decides. The shell acts.
**An example**:
Think of a function that renews expired subscriptions. The tangled version loads users from the database, checks dates against the system clock, charges cards and sends emails, all in one loop. The split version looks different:
1. The shell loads the subscriptions and reads the current date
2. The core takes those values and returns a list of actions: which subscriptions to renew, which to cancel, which emails to send
3. The shell executes that list
The core is now trivial to test: pass in values, check the returned values. No database, no mocks, no fake clock.
**Why it works**:
- **Fast, simple tests**: Most of the logic sits in pure functions, so most tests are plain input/output checks
- **Fewer mocks**: The shell is so thin that a few integration tests cover it
- **Easier reasoning**: Pure functions can be read in isolation (see pure functions and immutability)
- **Safer changes**: Side effects are concentrated in one place, so you know exactly where the risky code is
**How it connects**:
The pattern pairs naturally with Parse, Don't Validate: the shell parses messy external input into precise types, and the core only ever sees valid data. It's also a close cousin of hexagonal architecture and Clean Architecture, which push I/O to the edges for the same reasons.
**Keep in mind**:
Some programs are mostly shell (e.g., a script that copies files). Forcing a functional core there adds ceremony for nothing. The pattern pays off when there's real logic to protect.
Related Concepts
← Back to all concepts