Unit 2 of 2

Function Design

5 min Updated Jun 2026

A function is a promise. You give it something, it does one job, and it gives something back. When that promise is kept consistently, your code becomes easy to reason about. When it isn’t — when a function does three things, takes six parameters, and quietly modifies something it shouldn’t — bugs become hard to find and changes become risky.

This unit covers what makes a function well-designed.

One job

The most important rule in function design: a function should do one thing.

# Bad — does three things
FUNCTION processOrder(order):
    validate order fields
    calculate total with tax
    save to database
    RETURN confirmation

# Good — each function does one thing
FUNCTION validateOrder(order):
    RETURN true or false

FUNCTION calculateOrderTotal(order):
    RETURN total with tax

FUNCTION saveOrder(order):
    RETURN confirmation

The bad version is hard to name precisely — processOrder is vague because the function is vague. When each function does one job, names become obvious and the code reads like a list of clear instructions.

A useful test: if you can describe a function using the word “and”, it probably needs to be split up. “This function validates the order and calculates the total and saves it” — that’s three functions.

Keep it short

A function that fits on one screen is easier to understand than one that requires scrolling. If you find yourself writing a function longer than 20–30 lines, it’s usually doing more than one thing.

Short functions are also easier to test, easier to reuse, and easier to change without breaking something else.

Length is a symptom, not the root problem. Fix the root problem — too many responsibilities — and length takes care of itself.

Parameters: fewer is better

Every parameter a function takes is something the caller has to think about. The more parameters, the harder the function is to use correctly.

# Hard to call correctly — what order do these go in?
FUNCTION createUser(name, age, email, role, isActive, createdAt):
    ...

# Better — group related data
FUNCTION createUser(userDetails):
    ...

A common guideline: if a function needs more than three parameters, consider whether some of them belong together in a single object or structure. Grouping related data also means that if you need to add a new field later, you don’t have to change every place the function is called.

Also watch out for boolean parameters — a true or false passed into a function is often a sign it should be two separate functions:

# Bad — what does "true" mean here?
FUNCTION getUsers(includeInactive):
    ...

# Good
FUNCTION getActiveUsers():
    ...

FUNCTION getAllUsers():
    ...

Return something predictable

A function should always return the same type of thing. If it returns a number in one case and nothing in another, the caller has to handle both possibilities every time — which leads to cluttered, defensive code.

# Bad — returns a number or nothing
FUNCTION divide(a, b):
    IF b == 0:
        RETURN nothing
    RETURN a / b

# Better — always returns a number
FUNCTION divide(a, b):
    IF b == 0:
        RETURN 0
    RETURN a / b

The caller shouldn’t have to guess what they’re going to get back.

Avoid side effects

A side effect is anything a function does beyond computing and returning a value — writing to a file, modifying a variable outside itself, sending a network request, printing to the screen.

Side effects aren’t always avoidable, but they should be intentional and obvious. A function that silently modifies something outside itself is dangerous because it changes behaviour in ways the caller can’t see.

# Bad — modifies external state silently
FUNCTION applyDiscount(cart):
    cart.total = cart.total * 0.9   ← changes the cart the caller passed in
    RETURN cart.total

# Better — returns a new value, leaves the input unchanged
FUNCTION calculateDiscountedTotal(cartTotal):
    RETURN cartTotal * 0.9

The second version doesn’t touch the original data. The caller decides what to do with the result. That makes the function safer to use anywhere.

Pure functions

A pure function is one where the same input always produces the same output, and nothing outside the function is changed.

# Pure
FUNCTION square(n):
    RETURN n * n

# Not pure — result depends on something outside the function
FUNCTION getDiscount(price):
    RETURN price * currentDiscountRate   ← currentDiscountRate can change

Pure functions are the easiest kind to test (no setup required, no state to manage) and the easiest to reuse. They’re also completely predictable — you can call them anywhere, in any order, and they’ll behave the same way.

Not every function can be pure, but the more of your logic you can push into pure functions, the more stable your codebase becomes.

Don’t repeat yourself (DRY)

If you find yourself writing the same logic in two places, it belongs in a function.

# Bad — same logic copy-pasted
totalWithTax = subtotal * 1.15
deliveryWithTax = deliveryCost * 1.15
tipWithTax = tip * 1.15

# Good — logic lives in one place
FUNCTION addTax(amount):
    RETURN amount * 1.15

totalWithTax = addTax(subtotal)
deliveryWithTax = addTax(deliveryCost)
tipWithTax = addTax(tip)

The practical benefit: if the tax rate changes, you update one line instead of hunting through the codebase for every place you hardcoded 1.15.

What just happened?

A well-designed function does one thing, takes as few parameters as necessary, returns something predictable, and doesn’t secretly change state elsewhere. These constraints aren’t arbitrary — they each exist because violating them makes code harder to read, harder to test, and harder to change safely.

The underlying principle across all of them is the same: a function should be something you can trust completely without having to read its internals. When every function in a codebase earns that trust, the whole system becomes easier to work with.

In the next unit you’ll look at comments — when they help, when they’re a sign something else is wrong, and how to write ones that actually add value.