“Mocks are the mind-killer. They are the little-death that brings total production failure. I will face my mocks. I will permit them to pass over me and through me. And when they have gone past, I will turn the inner eye to see their path. Where the mocks have gone there will be nothing. Only pure data will remain.”
We’ve all walked the burning sands of the ‘Green Test, Broken Production’ mirage. Watching a sea of green is comforting, but it is a hollow victory. The true test comes when the code meets the deep desert of production and withers, leaving us to find that a mock ceased to reflect reality months ago.
I grew tired of mocks lying to me and the extra complexity they bring. This led me back to a classic design pattern: the Command.
The idea is simple: separate an action from its execution. I wanted a functional take on this, though: no classes, no mutation, just pure functions returning plain data. Most importantly, I wanted it without the academic vocabulary of category theory. I wanted something a developer could master in a single afternoon.
If a function performs I/O, it is by definition impure and may be non-deterministic. But if it returns a description of the I/O it intends to perform, it remains pure. Instead of the immediate await db.findUser(email), we return a Command that says: “I would like to find this user. Tell me when it is done.” That way, the business logic becomes a predictable stream of data transformations:
In the user registration example below, the database is never touched until the interpreter runEffect, the final arbiter, is invoked.
import { Success, Failure, Command, effectPipe, runEffect } from 'pure-effect';
// Pure functions: no I/O, no imports, instantly testable.
function validateRegistration(input) {
if (!input.email?.includes('@')) return Failure('Invalid email.');
if (input.password?.length < 8) return Failure('Password too short.');
return Success(input);
}
function ensureEmailAvailable(found) {
return found ? Failure('Email already in use.') : Success(true);
}
// Commands: side effects described as data, executed by the interpreter.
function findUserByEmail(email) {
const cmdFindUser = () => db.findUserByEmail(email);
return Command(cmdFindUser, (found) => Success(found));
}
function saveUser(input) {
const cmdSaveUser = () => db.saveUser(input);
return Command(cmdSaveUser, (saved) => Success(saved));
}
// Pipeline: compose into a single flow.
const registerUserFlow = (input) =>
effectPipe(
validateRegistration,
() => findUserByEmail(input.email),
ensureEmailAvailable,
() => saveUser(input)
)(input);
// Test: assert on structure, no mocking libraries, no I/O.
const flow = registerUserFlow({ email: '[email protected]', password: 'password123' });
assert.equal(flow.cmd.name, 'cmdFindUser');
assert.equal(flow.next(null).cmd.name, 'cmdSaveUser');
// Run: hand the tree to the interpreter at the boundary.
const saved = await runEffect(registerUserFlow(input), { flowName: 'register' });
Notice that the test only requires two assertions for a four-step pipeline. This is because effectPipe automatically handles the happy path. When a function returns a Success, the pipe immediately moves to the next step. It only pauses and returns a Command when it hits a side effect that requires the interpreter.
Decoupling logic from its immediate execution and treating it as a map of data presents several new possibilities:
-
No mocking libraries. You walk the tree in tests and assert on its structure:
assert.equal(flow.cmd.name, 'cmdFindUser')1. Nothing is executed. While you still need integration tests to ensure the interpreter correctly executes the dependencies, your business logic is freed from the brittle mocking setups in every single test file. -
Retry as data. Wrap any effect with
Retry(effect, { attempts: 3, delay: 200, backoff: 2 }). The configuration is plain data so you can assert on it in tests. No fake timers, no sleep hacks. -
Time-travel debugging. Every command’s input and output flows through the interpreter, so you get a full execution trace for free. You can write a simple timeTravel() function that replays it locally without touching any real infrastructure.
-
Runtime guardrails. An
onBeforeCommandhook sits between your business logic and the interpreter. It sees every intended side effect before it fires. Wire it once at startup and it applies everywhere, regardless of who wrote the calling code. -
AI-generated code you can actually verify. AI-generated async/await is a black box. You cannot know the side effects until they execute. Because this approach returns a map of data, you can programmatically inspect the AI’s plan and run it through a safety check before the interpreter fires a single real-world command.
I call this library Pure Effect. It is built around an API of six primitives 2: Success, Failure, Command, Ask, Retry, and Parallel. You compose your business logic using effectPipe and execute the final result using runEffect.
The entire package has zero dependencies and weighs in at under 1 KB minified and gzipped. My goal was not to create a massive ecosystem but to provide a lightweight pattern that fits into any existing codebase.
It’s impossible to talk about this space without mentioning Effect-TS. Effect-TS is a powerful, full-featured framework that provides fibers, structured concurrency, and complex schema validation. If you need those advanced capabilities, you should use it.
Pure Effect makes a different trade-off. It targets the 80% case: testable pipelines, dependency injection, and reliable retries. While Effect-TS is a framework you build your entire application around, Pure Effect is a pattern you drop into the code you are already writing. It requires no new vocabulary and no specialized knowledge of functional programming.
I’ve been using Pure Effect in production since December. It’s currently at version 0.8.0. While it has not reached 1.0 yet, it has proven stable enough for real-world use. I’m sharing it now because I want to hear how other developers might use this pattern to simplify their own testing and logic.
Please give Pure Effect a try and let me know what you think.
1
Pure Effect uses function names as identifiers to keep the API lean. For environments where minification is required, you can explicitly tag your commands.
2
The evolution of Pure Effect is documented in multiple blog posts.
Related: