Functional Programming Patterns

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.

When to Apply Functional Programming Patterns

Use these patterns when building systems where:

Pure Function Design

Avoid Side Effects in Functions

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;
}

Return Values Instead of Mutating Parameters

// ❌ 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 }));
}

Function Composition

Combine Simple Functions to Build Complex Behavior

// ✅ 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)

Partial Application for Reusable Components

// ✅ 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

Error Handling with Monads

Use Maybe Pattern for Optional Values

// ✅ 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);

Handle Errors with Either Pattern

// ✅ 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);
};

Immutable Data Structures

Create New Values Instead of Modifying Existing Ones

// ❌ Bad: mutation
const updatePerson = (person, newAge) => {
  person.age = newAge;
  return person;
};

// ✅ Good: immutable update
const updatePerson = (person, newAge) => ({
  ...person,
  age: newAge
});

Use Immutable Collections for Complex Updates

// ✅ 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;
};

Higher-Order Functions

Pass Functions as Arguments for Generic Behavior

// ✅ 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);

Implement Filter and Reduce for Data Processing

// ✅ 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);

Recursion Patterns

Implement Tail Call Optimization

// ❌ 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);
}

Use Accumulator Pattern for Efficiency

// ✅ 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]);
}

Configuration and State Management

Separate Configuration from Execution Logic

// ✅ Good: configuration-driven function
const createValidator = ({ min, max }) => {
  return (value) => value >= min && value <= max;
};

const ageValidator = createValidator({ min: 0, max: 120 });

Use Function Factories for Custom Behavior

// ✅ Good: factory functions for specialized behavior
const createLogger = (prefix) => {
  return (message) => {
    console.log(`${prefix}: ${message}`);
  };
};

const errorLogger = createLogger('ERROR');
const infoLogger = createLogger('INFO');

Common Failure Modes and Sharp Edges

Silent Mutation in Functional Code

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 }));
}

Incorrect Curry Implementation

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);
  };
};

Misuse of Maybe Pattern with Null Checks

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);
};

Incorrect Composition Order

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

Unhandled Edge Cases in Error Handling

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.