In the fast-paced world of software development, writing clean and maintainable code is a superpower. One of the most overlooked skills that contribute to this is the ability to write small, focused functions.

If you've ever stared at a tangled 200-line function and wondered how you got there — you're not alone. In this post, we’ll break down the value of small functions, show you how to refactor large ones, and explain how this all ties into the Single Responsibility Principle (SRP) at the function level.

Why Small Functions Matter

Small functions offer a host of benefits that go far beyond style:

  • Readability: Smaller blocks of logic are easier to scan and understand.
  • Testability: It’s simpler to write unit tests for isolated, single-purpose code.
  • Reusability: Focused functions are more likely to be reusable across the app.
  • Debuggability: When something breaks, it's faster to isolate the issue.

Ultimately, small functions lower cognitive load. They make it easier for your future self (and your teammates) to quickly grasp what’s happening — and that’s gold.

The Single Responsibility Principle for Functions

The Single Responsibility Principle (SRP) is one of the SOLID principles and states that a piece of code should have one reason to change. While SRP is typically discussed in the context of classes, it's just as useful when applied to functions.

Ask yourself:

Does this function do one thing, and do it well?

If the answer is “no,” it likely violates SRP and could be broken down.

A Before & After Example

Before: A Large, Messy Function

function processUserRegistration($request) {
    // Validate input
    if (empty($request['email']) || !filter_var($request['email'], FILTER_VALIDATE_EMAIL)) {
        throw new Exception('Invalid email');
    }
    if (strlen($request['password']) < 8) {
        throw new Exception('Password too short');
    }

    // Hash password
    $hashedPassword = password_hash($request['password'], PASSWORD_BCRYPT);

    // Save user
    $pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '');
    $stmt = $pdo->prepare('INSERT INTO users (email, password) VALUES (?, ?)');
    $stmt->execute([$request['email'], $hashedPassword]);

    // Send welcome email
    mail($request['email'], 'Welcome!', 'Thanks for signing up!');

    return true;
}

This function implements the process that is needed for registering a user in a fictional system.

Ask yourself before reading further: Does this function do one thing, and do it well?

After: Small, Focused Functions

Below, we take the messy, all-in-one function and break it down step by step. At each stage, we apply clean code principles and move closer to small, testable, maintainable code.

Step 1: Identify Distinct Responsibilities

Let’s revisit the original function:

function processUserRegistration($request) {
    // 1. Validate input
    if (empty($request['email']) || !filter_var($request['email'], FILTER_VALIDATE_EMAIL)) {
        throw new Exception('Invalid email');
    }
    if (strlen($request['password']) < 8) {
        throw new Exception('Password too short');
    }

    // 2. Hash password
    $hashedPassword = password_hash($request['password'], PASSWORD_BCRYPT);

    // 3. Save user
    $pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '');
    $stmt = $pdo->prepare('INSERT INTO users (email, password) VALUES (?, ?)');
    $stmt->execute([$request['email'], $hashedPassword]);

    // 4. Send email
    mail($request['email'], 'Welcome!', 'Thanks for signing up!');

    return true;
}

We clearly see four distinct operations:

  1. Input validation
  2. Password hashing
  3. Database interaction
  4. Email sending

This is our first insight: each of these responsibilities deserves its own function.

Step 2: Extract the Validation Logic

We start by isolating the validation logic into a function called validateRequest():

function validateRequest($request) {
    if (empty($request['email']) || !filter_var($request['email'], FILTER_VALIDATE_EMAIL)) {
        throw new Exception('Invalid email');
    }
    if (strlen($request['password']) < 8) {
        throw new Exception('Password too short');
    }
}

Now we can call this from our main function:

function processUserRegistration($request) {
    validateRequest($request);
    // other logic here...
}

Why this helps: We can now unit test validation independently. It also makes processUserRegistration()immediately more readable.

Step 3: Extract Password Hashing

Next, we isolate the logic that hashes the password:

function hashPassword($password) {
    return password_hash($password, PASSWORD_BCRYPT);
}

Update the main function:

function processUserRegistration($request) {
    validateRequest($request);
    $hashedPassword = hashPassword($request['password']);
    // continue...
}

Why this helps: This makes future changes (e.g., switching to Argon2 or adding salting logic) easier to implement and test in isolation.

Step 4: Extract User Persistence

Now, extract the database logic into a saveUser() function:

function saveUser($email, $hashedPassword) {
    $pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '');
    $stmt = $pdo->prepare('INSERT INTO users (email, password) VALUES (?, ?)');
    $stmt->execute([$email, $hashedPassword]);
}

Main function update:

function processUserRegistration($request) {
    validateRequest($request);
    $hashedPassword = hashPassword($request['password']);
    saveUser($request['email'], $hashedPassword);
    // continue...
}

Why this helps: Database logic is now encapsulated. You can swap PDO for an ORM, wrap in a transaction, or mock this in tests.

Step 5: Extract Email Sending

Last, extract the mailing logic:

function sendWelcomeEmail($email) {
    mail($email, 'Welcome!', 'Thanks for signing up!');
}

And finally update the main flow:

function processUserRegistration($request) {
    validateRequest($request);
    $hashedPassword = hashPassword($request['password']);
    saveUser($request['email'], $hashedPassword);
    sendWelcomeEmail($request['email']);
    return true;
}

Why this helps: Future changes to email content, templating, or queueing systems (like Symfony Messenger) are localized to this function.

Final Refactored Version

Here’s the clean result of our step-by-step refactor:

function processUserRegistration($request) {
    validateRequest($request);
    $hashedPassword = hashPassword($request['password']);
    saveUser($request['email'], $hashedPassword);
    sendWelcomeEmail($request['email']);
    return true;
}

function validateRequest($request) {
    if (empty($request['email']) || !filter_var($request['email'], FILTER_VALIDATE_EMAIL)) {
        throw new Exception('Invalid email');
    }
    if (strlen($request['password']) < 8) {
        throw new Exception('Password too short');
    }
}

function hashPassword($password) {
    return password_hash($password, PASSWORD_BCRYPT);
}

function saveUser($email, $hashedPassword) {
    $pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '');
    $stmt = $pdo->prepare('INSERT INTO users (email, password) VALUES (?, ?)');
    $stmt->execute([$email, $hashedPassword]);
}

function sendWelcomeEmail($email) {
    mail($email, 'Welcome!', 'Thanks for signing up!');
}

Each function is clear, purposeful, and easy to maintain.

Easier to Test and Maintain

Want to test validation logic? You can now write focused unit tests for validateRequest() without touching the database or email layer.

Want to refactor the storage layer to use an ORM later? You only need to touch saveUser().

These small changes drastically reduce friction in future development.

When to Break Up Functions

Here are some signals that your function might be too big:

  • It does multiple things (e.g. validation + DB access).
  • You struggle to name it concisely.
  • You need to mock many dependencies to test it.
  • It contains multiple levels of abstraction.

If any of these apply, start breaking the function down until each one has a single, obvious responsibility.

Avoid Going Too Far

Not every function needs to be one-liner minimalist art.

If breaking down a function introduces unnecessary indirection, slows down comprehension, or requires jumping between too many files — you might be over-optimizing.

The goal is clarity, not fragmentation.

  • Aim for 5-20 lines per function. (This is a rule of thumbs.)
  • Each function should do one thing.
  • If in doubt, name it and see if the name makes sense on its own.

Tools That Can Help

  • PHPStan / Psalm: These static analyzers help identify overly complex or poorly structured functions.
  • PHP CS Fixer / PHP_CodeSniffer: Linting tools that enforce formatting, which can encourage simpler, more readable code.
  • IDE Metrics: Most modern editors like PhpStorm can show you cyclomatic complexity and help spot functions that do too much.

Bonus Tip: Don't Refactor Blindly

Refactoring should be safe and test-driven when possible. Use this process when:

  • You have unit tests or are adding them as you go.
  • You’re introducing changes that need to be isolated and low-risk.
  • You're onboarding new developers (clear structure aids understanding).

Summary

Writing small, focused functions isn’t just a coding style — it’s a mindset. It’s about designing code that’s easy to understand, test, and evolve. By applying the Single Responsibility Principle at the function level, you create a codebase that’s easier to navigate and safer to change.

“Functions should do one thing. They should do it well. They should do it only.”
— Robert C. Martin

So next time you’re about to write that 60-line method, pause. Ask yourself:
“What’s the one thing this function should really do?”

Your future self will thank you.