Production-grade guide to functional programming patterns covering architecture patterns, implementation strategies, testing approaches, and operational best practices for enterprise engineering teams.
This reference describes practical patterns for functional programming in software systems, focusing on how to structure computation and data flow using pure functions, immutability, and declarative composition. These patterns apply when writing code that emphasizes function purity, predictable behavior, and compositional design.
Use these patterns when building systems where:
Functions must not modify external state or depend on external mutable data.
// ❌ Bad: function with side effect
function addTax(price, taxRate) {
const tax = price * taxRate;
globalTaxRate = taxRate; // modifies external state
return price + tax;
}
// ✅ Good: pure function
function addTax(price, taxRate) {
const tax = price * taxRate;
return price + tax;
}
// ❌ Bad: mutating input
function processItems(items) {
items.forEach(item => item.processed = true);
return items;
}
// ✅ Good: returning new values
function processItems(items) {
return items.map(item => ({ ...item, processed: true }));
}
// ✅ Good: compose functions using pipeline pattern
const pipe = (f, g) => x => g(f(x));
const addOne = x => x + 1;
const multiplyByTwo = x => x * 2;
const square = x => x * x;
const transform = pipe(addOne, multiplyByTwo, square);
console.log(transform(3)); // 64 (3+1=4, 4*2=8, 8^2=64)
// ✅ Good: partial application
const curry = (fn, ...args) => {
return (...moreArgs) => {
const allArgs = [...args, ...moreArgs];
if (allArgs.length >= fn.length) {
return fn(...allArgs);
}
return curry(fn, ...allArgs);
};
};
const add = (a, b) => a + b;
const addTen = curry(add, 10);
console.log(addTen(5)); // 15
// ✅ Good: simple Maybe implementation
class Maybe {
constructor(value) {
this.value = value;
}
static of(value) {
return new Maybe(value);
}
static nothing() {
return new Maybe(null);
}
map(fn) {
if (this.value === null || this.value === undefined) {
return Maybe.nothing();
}
return Maybe.of(fn(this.value));
}
flatMap(fn) {
if (this.value === null || this.value === undefined) {
return Maybe.nothing();
}
return fn(this.value);
}
}
// Usage example
const findUser = id => Maybe.of(users.find(u => u.id === id));
const getUserEmail = user => Maybe.of(user.email);
const processEmail = email => Maybe.of(email.toLowerCase());
const result = findUser(123)
.flatMap(getUserEmail)
.map(processEmail);
// ✅ Good: Either pattern for error handling
class Either {
constructor(value) {
this.value = value;
}
static left(error) {
return new Either({ isLeft: true, value: error });
}
static right(value) {
return new Either({ isLeft: false, value });
}
map(fn) {
if (this.value.isLeft) return this;
return Either.right(fn(this.value.value));
}
flatMap(fn) {
if (this.value.isLeft) return this;
return fn(this.value.value);
}
}
// Usage example
const divide = (a, b) => {
if (b === 0) return Either.left('Division by zero');
return Either.right(a / b);
};
// ❌ Bad: mutation
const updatePerson = (person, newAge) => {
person.age = newAge;
return person;
};
// ✅ Good: immutable update
const updatePerson = (person, newAge) => ({
...person,
age: newAge
});
// ✅ Good: nested immutable updates
const updateNestedObject = (obj, path, value) => {
const keys = path.split('.');
const lastKey = keys.pop();
return keys.reduce((acc, key) => ({
...acc,
[key]: { ...acc[key] }
}), obj)[lastKey] = value;
};
// ✅ Good: map function that works with any transformation
const processList = (list, transform) => {
return list.map(transform);
};
const double = x => x * 2;
const numbers = [1, 2, 3, 4];
const doubled = processList(numbers, double);
// ✅ Good: generic filtering
const filterEven = numbers => numbers.filter(n => n % 2 === 0);
// ✅ Good: generic reduction
const sum = numbers => numbers.reduce((acc, n) => acc + n, 0);
// ❌ Bad: regular recursion (may cause stack overflow)
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
// ✅ Good: tail recursive version
function factorial(n, acc = 1) {
if (n <= 1) return acc;
return factorial(n - 1, n * acc);
}
// ✅ Good: accumulate results in recursion
function sumRecursive(numbers, index = 0, acc = 0) {
if (index >= numbers.length) return acc;
return sumRecursive(numbers, index + 1, acc + numbers[index]);
}
// ✅ Good: configuration-driven function
const createValidator = ({ min, max }) => {
return (value) => value >= min && value <= max;
};
const ageValidator = createValidator({ min: 0, max: 120 });
// ✅ Good: factory functions for specialized behavior
const createLogger = (prefix) => {
return (message) => {
console.log(`${prefix}: ${message}`);
};
};
const errorLogger = createLogger('ERROR');
const infoLogger = createLogger('INFO');
Functions may appear pure but still mutate external state.
// ❌ Silent mutation can break purity
function processUserData(users) {
users.forEach(user => user.processed = true); // modifies input array
return users.map(user => ({ ...user, id: user.id }));
}
Partial application fails silently with incorrect implementation.
// ❌ Bad curry that doesn't preserve function length
const badCurry = (fn, ...args) => (...moreArgs) => {
const allArgs = [...args, ...moreArgs];
return fn(...allArgs); // loses original function signature
};
// ✅ Good curry preserves function length
const goodCurry = (fn, ...args) => {
return (...moreArgs) => {
const allArgs = [...args, ...moreArgs];
if (allArgs.length >= fn.length) {
return fn(...allArgs);
}
return goodCurry(fn, ...allArgs);
};
};
The Maybe pattern works best when all values are explicitly wrapped.
// ❌ Bad: mixing Maybe and null checks
const getUserName = user => {
if (!user) return Maybe.nothing(); // good
return Maybe.of(user.name); // good
};
// ✅ Good: consistent Maybe usage
const getUserName = user => {
if (!user || !user.name) return Maybe.nothing();
return Maybe.of(user.name);
};
Function composition order affects results when functions are not commutative.
// ❌ Bad: wrong composition order
const composeRight = (f, g) => x => f(g(x));
const addOne = x => x + 1;
const multiplyByTwo = x => x * 2;
const badTransform = composeRight(multiplyByTwo, addOne); // 3 + 1 = 4, then 4 * 2 = 8
console.log(badTransform(3)); // 8
// ✅ Good: correct order
const composeLeft = (f, g) => x => g(f(x));
const goodTransform = composeLeft(addOne, multiplyByTwo); // 3 * 2 = 6, then 6 + 1 = 7
console.log(goodTransform(3)); // 7
Either pattern implementations often miss error recovery paths.
// ❌ Bad: incomplete error handling
const processWithErrors = data => {
if (!data) return Either.left('Missing data');
return Either.right(data.value);
};
// ✅ Good: comprehensive error handling
const processWithErrors = data => {
if (!data) return Either.left('Missing data');
if (typeof data !== 'object') return Either.left('Invalid data type');
if (!data.value) return Either.left('Missing value field');
return Either.right(data.value);
};
These patterns provide a foundation for writing functional code that is predictable, testable, and maintainable. The key is consistency in applying these principles across your system to achieve maximum benefit from functional design.
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy