cd /news/developer-tools/designing-functions-that-compound-sm… · home topics developer-tools article
[ARTICLE · art-109870] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Designing Functions That Compound: Small, Pure, and Composable

A developer advocates for designing functions that are small, pure, and composable to improve code maintainability and testability. The approach emphasizes pure functions without side effects, single-responsibility functions, and composition using utilities like pipe and currying. Practical JavaScript examples illustrate how these principles lead to more reliable and reusable code.

read5 min views2 publishedAug 25, 2026

When I first started writing code, I treated functions like isolated tasks: parse this input, format that output, save this record. Each function worked, but the codebase became a tangled mess of duplicated logic and hidden side effects. The turning point was learning to design functions that compound: small, pure, and composable units that build on each other like Lego bricks.

Compounding functions aren't just about reuse. They make reasoning easier, testing trivial, and refactoring safe. Here's how I approach it.

A pure function is one that always returns the same output for the same input and has no side effects (no I/O, no mutation of external state). Purity is the foundation of composability because you can trust it. If a function mutates something, you can't safely combine it with others without worrying about order and hidden dependencies.

Here's a simple example in JavaScript:

// impure: mutates the array
function addToCart(cart, item) {
  cart.push(item);
  return cart;
}

// pure: returns a new array
function addToCart(cart, item) {
  return [...cart, item];
}

The pure version can be used in any context without surprising the caller. It also makes testing trivial: no setup, no cleanup, just pass inputs and check outputs.

A function should do one thing and do it well. If you find yourself writing "and" in the function name (e.g., parseAndValidate

), split it. Small functions are easier to reason about, test, and combine. They also tend to have clearer names, which is documentation in itself.

Consider this overblown function:

function processOrder(order) {
  // 30 lines of logic: validate, apply discount, calculate tax, format output
}

Break it down:

function validateOrder(order) { /* ... */ }
function applyDiscount(order, discount) { /* ... */ }
function calculateTax(subtotal) { /* ... */ }
function formatOrder(order) { /* ... */ }

n

Now each piece is testable in isolation and can be reused elsewhere. The composition happens at a higher level, like a pipeline.

Once you have small, pure functions, composition becomes natural. You can chain them, either manually or with a utility like pipe

(available in lodash, Ramda, or you can write a tiny one yourself).

function pipe(...fns) {
  return (arg) => fns.reduce((acc, fn) => fn(acc), arg);
}

const processOrder = pipe(
  validateOrder,
  (order) => applyDiscount(order, order.coupon),
  calculateTax,
  formatOrder
);

This reads like a recipe. Each step is a function that takes the output of the previous one. If you need to add a step, you just insert another function. If a step fails, you know exactly where to look.

A common mistake is writing functions that require too much context. For example, a function that takes an entire object but only uses one property. Instead, design functions to accept exactly what they need and return what makes sense for the next step.

// less composable
function getFullName(user) {
  return `${user.firstName} ${user.lastName}`;
}

// more composable
function getFullName(firstName, lastName) {
  return `${firstName} ${lastName}`;
}

The second version can be used in map

or reduce

more easily, and it doesn't couple you to a specific data shape.

Currying lets you fix some arguments and get a new function for the rest. It's a powerful way to create specialized versions of generic functions, which then compose beautifully.

function multiply(a, b) {
  return a * b;
}

function curry2(fn) {
  return (a) => (b) => fn(a, b);
}

const multiplyCurried = curry2(multiply);
const double = multiplyCurried(2);
const triple = multiplyCurried(3);

[1, 2, 3].map(double); // [2, 4, 6]

Now double

and triple

are reusable, composable pieces. You can combine them with other functions without rewriting logic.

When functions are pure and small, testing becomes a joy. You write a test per function with a few inputs and expected outputs. No mocking, no setup. And because functions compose, you can test the composition as a whole by simply testing the pipeline with realistic inputs.

Here's a quick test for a pure function:

// test.js
test('calculateTax applies 10% rate', () => {
  expect(calculateTax(100)).toBe(10);
});

That's it. No database, no network, no global state. The confidence you get from these tests compounds: you can refactor the internals of a function without worrying about breaking the system, as long as the contract holds.

Compounding doesn't mean abstracting everything into tiny pieces. Sometimes a function with a few lines is fine. The goal is to keep each unit at a level where it's obvious what it does. If you find yourself creating functions with names like doThing

or process

that just call other functions, you might be over-engineering.

A good heuristic: if a function is so short that it's just a wrapper, consider inlining it unless it provides a clear conceptual abstraction.

If you have legacy code, start by identifying impure functions and converting them to pure ones. Then extract logical blocks into separate functions. Finally, compose them with a pipeline. Do it incrementally, one function at a time, and let tests guide you.

For example, I once had a massive handleRequest

function that did validation, database access, and response formatting. I extracted the validation into a pure function, then the formatting, and left the side-effectful parts at the edges. The core logic became testable without a server.

Designing functions that compound isn't just a style choice. It's a discipline that pays off in every aspect of development: readability, maintainability, testing, and debugging. When each function is a small, pure, composable unit, your codebase becomes a toolkit of reliable building blocks. You start to see patterns, reuse becomes effortless, and new features feel like assembling existing pieces rather than writing fresh code from scratch.

Start small. Pick one function in your codebase that's doing too much, break it down, and compose it back together. The effect will surprise you, and it grows with every function you clean up. That's the compounding power of good design.

── more in #developer-tools 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/designing-functions-…] indexed:0 read:5min 2026-08-25 ·