Skip to main content
Technology & EngineeringClean Code198 lines

Function Design

Design small, focused functions that do one thing well and are easy to test

Quick Summary27 lines
You are an expert in function design for writing clean, maintainable code.

## Key Points

- **Do one thing**: A function should perform a single, well-defined task. If you can extract another meaningful function from it, it is doing more than one thing.
- **One level of abstraction**: Statements within a function should all be at the same level of abstraction. Do not mix high-level policy with low-level detail.
- **Small size**: Functions should typically be 5 to 15 lines. If a function exceeds 20 lines, look for extraction opportunities.
- **Minimal arguments**: Prefer zero to two arguments. Three or more suggest the arguments should be grouped into an object.
- **No side effects**: A function that claims to do one thing should not secretly do something else. If side effects are necessary, name the function to make them explicit.
- **Command-query separation**: A function should either change state (command) or return information (query), not both.
- Name functions with verbs that describe what they do, not how
- A function that returns a boolean should read as a question: `is_valid()`, `has_access()`
- Prefer returning values over mutating arguments
- Use early returns to eliminate nested conditionals
- Keep functions pure where possible — same input always produces same output
- **Hidden side effects**: A `validate()` function that also modifies the object it validates

## Quick Example

```typescript
function createUser(
  name: string, email: string, age: number,
  role: string, department: string, startDate: Date
): User { ... }
```
skilldb get clean-code-skills/Function DesignFull skill: 198 lines
Paste into your CLAUDE.md or agent config

Function Design — Clean Code

You are an expert in function design for writing clean, maintainable code.

Core Philosophy

A well-designed function is a unit of thought. It takes a clearly named concept from the problem domain and gives it a precise, testable implementation. When functions are small, focused, and operate at a single level of abstraction, the code reads like a narrative: high-level functions describe what the program does in business terms, and each layer below fills in progressively more technical detail. This top-down readability is not a luxury — it is what makes code maintainable by humans who did not write it.

The most important constraint on function design is doing exactly one thing. "One thing" does not mean "one line" — it means one level of abstraction, one reason to exist, one concept that the function name can honestly describe. If you find yourself struggling to name a function without using "and" or "then," it is doing more than one thing. If you can extract a meaningful sub-function from it, the original was mixing abstraction levels.

Side effects are the hidden cost of bad function design. A function named validateOrder that also saves to the database and sends an email is lying about what it does. Every caller is now coupled to those hidden behaviors, and testing requires mocking infrastructure that the function's signature never mentions. Clean functions make their effects explicit: either through return values, through names that honestly describe the mutation (saveAndNotify), or by separating queries from commands entirely.

Anti-Patterns

  • Boolean flag parameters that fork the function into two behaviors: A parameter like isAdmin that causes the function to execute completely different logic paths is a sign that two separate functions are being forced into one. Split them and let the caller choose the right function explicitly.

  • Functions with more than three parameters: Long parameter lists are hard to remember, easy to get wrong (especially when multiple parameters share the same type), and signal that the function is doing too much. Group related parameters into an object or struct.

  • Mixing abstraction levels within a single function: A function that calls fetchUserFromDatabase() on one line and manually parses a date string on the next is forcing the reader to context-switch between high-level intent and low-level detail. Extract the low-level work into a named helper.

  • Returning null to signal absence: Returning null forces every caller to add a null check, and a forgotten check produces a null reference error far from the actual cause. Use exceptions for unexpected absence, empty collections for expected empty results, or option/result types for explicitly optional values.

  • Functions with hidden side effects: A function named getUser that also increments a counter, writes to a cache, or logs analytics data is violating the principle of least surprise. Side effects should be visible in the function's name and signature, not discovered through debugging.

Overview

Functions are the first line of organization in any program. Well-designed functions are small, do one thing, operate at a single level of abstraction, and have clear inputs and outputs. They are the building blocks of readable, testable, and maintainable software.

Core Principles

  • Do one thing: A function should perform a single, well-defined task. If you can extract another meaningful function from it, it is doing more than one thing.
  • One level of abstraction: Statements within a function should all be at the same level of abstraction. Do not mix high-level policy with low-level detail.
  • Small size: Functions should typically be 5 to 15 lines. If a function exceeds 20 lines, look for extraction opportunities.
  • Minimal arguments: Prefer zero to two arguments. Three or more suggest the arguments should be grouped into an object.
  • No side effects: A function that claims to do one thing should not secretly do something else. If side effects are necessary, name the function to make them explicit.
  • Command-query separation: A function should either change state (command) or return information (query), not both.

Implementation Patterns

Long Function — Before

def process_order(order):
    # Validate
    if not order.items:
        raise ValueError("Order must have items")
    if not order.customer:
        raise ValueError("Order must have a customer")
    for item in order.items:
        if item.quantity <= 0:
            raise ValueError(f"Invalid quantity for {item.name}")

    # Calculate totals
    subtotal = sum(i.price * i.quantity for i in order.items)
    tax = subtotal * 0.08
    shipping = 5.99 if subtotal < 50 else 0
    total = subtotal + tax + shipping

    # Save
    order.subtotal = subtotal
    order.tax = tax
    order.shipping = shipping
    order.total = total
    order.status = "confirmed"
    db.save(order)

    # Notify
    email.send(order.customer.email, f"Order confirmed: ${total:.2f}")
    return order

Extracted Functions — After

def process_order(order):
    validate_order(order)
    totals = calculate_totals(order.items)
    confirm_order(order, totals)
    notify_customer(order)
    return order

def validate_order(order):
    if not order.items:
        raise ValueError("Order must have items")
    if not order.customer:
        raise ValueError("Order must have a customer")
    for item in order.items:
        if item.quantity <= 0:
            raise ValueError(f"Invalid quantity for {item.name}")

def calculate_totals(items):
    subtotal = sum(i.price * i.quantity for i in items)
    tax = subtotal * 0.08
    shipping = 5.99 if subtotal < 50 else 0
    return Totals(subtotal=subtotal, tax=tax, shipping=shipping)

def confirm_order(order, totals):
    order.subtotal = totals.subtotal
    order.tax = totals.tax
    order.shipping = totals.shipping
    order.total = totals.total
    order.status = "confirmed"
    db.save(order)

def notify_customer(order):
    email.send(order.customer.email, f"Order confirmed: ${order.total:.2f}")

Too Many Parameters — Before

function createUser(
  name: string, email: string, age: number,
  role: string, department: string, startDate: Date
): User { ... }

Parameter Object — After

interface CreateUserRequest {
  name: string;
  email: string;
  age: number;
  role: string;
  department: string;
  startDate: Date;
}

function createUser(request: CreateUserRequest): User { ... }

Flag Arguments — Before

def render_page(page, is_admin):
    if is_admin:
        render_admin_toolbar(page)
        render_admin_content(page)
    else:
        render_user_content(page)

Separate Functions — After

def render_admin_page(page):
    render_admin_toolbar(page)
    render_admin_content(page)

def render_user_page(page):
    render_user_content(page)

Stepdown Rule

Arrange functions so the code reads top-down. Each function is followed by the functions it calls, at the next level of abstraction.

# Level 0 — orchestration
def generate_monthly_report(month):
    data = gather_report_data(month)
    summary = summarize(data)
    return format_report(summary)

# Level 1 — mid-level operations
def gather_report_data(month):
    transactions = fetch_transactions(month)
    returns = fetch_returns(month)
    return merge(transactions, returns)

# Level 2 — low-level details
def fetch_transactions(month):
    return db.query("SELECT * FROM transactions WHERE month = ?", month)

Best Practices

  • Name functions with verbs that describe what they do, not how
  • A function that returns a boolean should read as a question: is_valid(), has_access()
  • Prefer returning values over mutating arguments
  • Use early returns to eliminate nested conditionals
  • Keep functions pure where possible — same input always produces same output

Common Pitfalls

  • Hidden side effects: A validate() function that also modifies the object it validates
  • Boolean flag parameters: They signal the function does two things — split it instead
  • Output arguments: Passing an object to be mutated is less clear than returning a new value
  • Deeply nested logic: More than two levels of indentation usually means the function should be decomposed
  • Dead parameters: Arguments that are accepted but never used; remove them
  • Returning error codes instead of throwing: Leads to deeply nested if/else chains at every call site

Install this skill directly: skilldb add clean-code-skills

Get CLI access →