Aikido

How to keep functions concise: writing maintainable code

Readability

Rule

Keep functions concise.
Long functions are difficult to understand, test, and maintain.

Supported languages: 45+

Introduction

Functions spanning hundreds of lines mix multiple responsibilities, making it hard to understand what the function does without reading every line. Long functions typically handle multiple concerns like validation, business logic, data transformation, and error handling all in one place. This violates the single responsibility principle and creates code that's difficult to test, debug, and modify without breaking existing behavior.

Why it matters

Code maintainability: Long functions require developers to hold more context in their head to understand behavior. Modifying one part risks breaking another because all logic is intertwined. Bug fixes become risky as unintended side effects are hard to predict.

Testing complexity: Testing a 200-line function means covering all possible code paths in one test, requiring complex setup and numerous test cases. Smaller functions can be tested independently with focused unit tests, making test suites faster and more reliable.

Code examples

❌ Non-compliant:

async function processOrder(orderData) {
    if (!orderData.items?.length) throw new Error('Items required');
    if (!orderData.customer?.email) throw new Error('Email required');
    const subtotal = orderData.items.reduce((sum, item) => 
        sum + (item.price * item.quantity), 0);
    const tax = subtotal * 0.08;
    const total = subtotal + tax + (subtotal > 50 ? 0 : 9.99);
    const order = await db.orders.create({
        customerId: orderData.customer.id,
        total: total
    });
    await emailService.send(orderData.customer.email, `Order #${order.id}`);
    await inventory.reserve(orderData.items);
    return order;
}

Why it's wrong: This function handles validation, calculation, database operations, email, and inventory. Testing requires mocking all dependencies. Any change to tax logic or validation requires modifying this entire function.

✅ Compliant:

function validateOrder(orderData) {
    if (!orderData.items?.length) throw new Error('Items required');
    if (!orderData.customer?.email) throw new Error('Email required');
}

function calculateTotal(items) {
    const subtotal = items.reduce((sum, item) => 
        sum + (item.price * item.quantity), 0);
    return subtotal + (subtotal * 0.08) + (subtotal > 50 ? 0 : 9.99);
}

async function createOrder(customerId, total) {
    return await db.orders.create({ customerId, total });
}

async function processOrder(orderData) {
    validateOrder(orderData);
    const total = calculateTotal(orderData.items);
    const order = await createOrder(orderData.customer.id, total);
    
    // Non-critical operations in background
    emailService.send(orderData.customer.email, `Order #${order.id}`).catch(console.error);
    
    return order;
}

Why this matters: Each function has one clear responsibility. validateOrder() and calculateTotal() can be tested independently without mocks. createOrder() isolates database logic. Email and inventory operations don't block order creation, and failures are handled separately.

Conclusion

Evolve APIs through additive changes: add new fields, add new endpoints, add optional parameters. When breaking changes are unavoidable, use API versioning to run old and new versions concurrently. Deprecate old fields with clear timelines and migration guides before removing them.

FAQs

Got Questions?

How do I break down long functions?

Identify distinct responsibilities within the function. Extract validation into separate functions. Pull out calculations into pure functions. Move I/O operations (database, API calls) into their own functions. Each extracted function should have a clear, single purpose with a descriptive name.

Don't small functions add overhead and hurt performance?

Modern compilers and interpreters inline small functions, eliminating call overhead. The performance impact is negligible compared to maintainability benefits. Profile before optimizing. Readable code is easier to optimize later when you identify actual bottlenecks.

What about functions with lots of sequential steps?

Sequential steps suggest a workflow that can be broken into smaller functions. Create helper functions for each step and call them in sequence from a coordinator function. This makes the workflow readable and each step testable independently.

How do I handle functions that need many parameters after extraction?

Pass objects containing related parameters rather than long parameter lists. Or consider whether extracted functions should be methods on a class that holds shared state. If a function needs 6+ parameters, it might indicate poor abstraction or missing data structures.

Should I extract functions even if they're only called once?

Yes, if extraction improves readability. A well-named extracted function documents what a code block does better than comments. One-off extraction is valuable when it clarifies complex logic or reduces nesting levels in the parent function.

Get secure for free

Secure your code, cloud, and runtime in one central system.
Find and fix vulnerabilities fast automatically.

No credit card required | Scan results in 32secs.