You've probably written a function that started out small and innocent. It calculated a total. Then someone asked it to format the result for display. Then it needed to send a confirmation email. Then it started logging transactions. Before you knew it, one little function was doing the work of four — and breaking in ways you couldn't predict.

This is one of the most common traps in software development. Functions accumulate responsibilities the way a junk drawer accumulates odds and ends. The single responsibility principle offers a clear remedy: each function should do exactly one thing. But figuring out what one thing actually means is where most developers get stuck. Let's untangle it.

What 'One Thing' Actually Means

When software engineers say a function should do 'one thing,' it sounds deceptively simple. But in practice, the boundary between one responsibility and two gets blurry fast. Consider a function called processOrder. It validates the customer's input, calculates the order total, and saves everything to a database. Is that one thing — processing an order — or three entirely separate things crammed into one place?

Here's a practical test you can use right away. Try describing what your function does without using the word 'and.' If you can't do it, the function is probably juggling multiple responsibilities. 'This function validates the order' passes the test. 'This function validates the order and saves it to the database' reveals a clear split. That little word 'and' is your early warning system.

You can also check whether your function operates at a single level of abstraction. Calculating a subtotal is one level. Formatting that subtotal for a web page is a completely different level. When you mix abstraction levels inside one function, your brain has to constantly shift gears between different kinds of thinking. That mental gear-shifting is exactly where bugs love to hide.

Takeaway

If you need the word 'and' to describe what a function does, it's doing more than one thing. That one-word test will catch most violations before they become problems.

Building Complex Behavior from Simple Pieces

Once you commit to writing small, focused functions, a natural question follows: how do you actually build anything useful? Real software needs to validate data, run calculations, format output, and save results to a database. If every function only does one thing, don't you end up with hundreds of tiny scattered pieces? You do end up with many pieces — and that's exactly the point. The power lies in composition.

Think of it like following a recipe. A good recipe doesn't say 'make dinner.' It says chop the vegetables, heat the oil, sauté for three minutes, then add seasoning. Each step is simple and self-contained. The complete meal emerges from combining those steps in the right order. Your code can work the same way.

Instead of one massive processOrder function, you write smaller ones: validateOrder, calculateTotal, applyDiscount, and saveOrder. A higher-level function calls each one in sequence. This orchestrating function reads like a table of contents — you can see the entire workflow at a glance without drowning in details. When you need to change how discounts work, you go straight to applyDiscount. Nothing else is affected.

Takeaway

Complex software isn't built from complex functions — it's built from simple functions combined thoughtfully. The art is in the arrangement, not the size of the individual pieces.

Why Small Functions Make Testing Effortless

This is where single responsibility truly proves its worth. Testing a function that does one thing is straightforward — you know exactly what inputs to provide and what output to expect. Testing a function that handles five responsibilities at once quickly becomes a nightmare where you're fighting your own code just to verify it works.

Imagine a function that validates user input, queries a database, performs a calculation, and sends an email notification. To test just the calculation, you need to set up valid input data, mock the database connection, and intercept the email. That's an enormous amount of ceremony for checking some basic math. Most developers skip writing that test entirely — and that's exactly how bugs slip through unnoticed.

Now imagine the calculation lives in its own small function. You pass in two numbers, you get a result back. You can write ten test cases in the time it took to set up one test for the bloated version. When something breaks, the failing test tells you exactly where the problem lives. No detective work. No stepping through a hundred lines of unrelated logic. Just a clear signal pointing to a clear fix.

Takeaway

If a function is hard to test, that's not a testing problem — it's a design problem. Easy-to-test code and well-designed code are almost always the same thing.

Writing functions that do one thing isn't about being pedantic. It's about giving yourself — and every developer who touches your code after you — a fighting chance at understanding what's happening. Small functions are easier to name, easier to test, and easier to change without breaking something unexpected.

Pick any function in your current project that feels complicated. Try describing it without the word 'and.' If you can't, you've found your first candidate for splitting. One function, one job, one less thing to worry about.