skip to content

Small Functions

Functions that do one thing, at one level of abstraction, with short argument lists and no hidden side effects. You will learn why flag arguments and long parameter lists are smells, and how the step-down rule keeps a file readable top to bottom.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

6

In function design, what does the guideline "a function should do one thing" actually mean, and how can you tell that a given function violates it?

level: juniorimportance: must knowfreq 78%

answer

  1. One thing = one level of abstraction
  2. Extract test: new name ≠ restatement
  3. Name needs "and" → two things
  4. Section comments mark extraction points
  5. One reason to change (SRP at function scale)

basics

~20 s

It means the function performs a single, clearly nameable job. Signs of violation: you can extract a meaningful chunk into another function that isn't just restating the original, the name needs "and"/"or", or the body mixes unrelated steps.

solid answer

~50 s

"Do one thing" means everything inside the function sits at one purpose and one level of abstraction: the body reads as the steps of a single named idea, not as several independent jobs stitched together. Practical tests: (1) can you extract a block into a new function whose name is not merely a restatement of the original? If yes, the original did more than one thing. (2) Does an honest name require "and", "or", "then", or "handle"? (3) Do sections need comments like "// now validate", "// now save"? Those comments are extraction points. (4) Does it both decide and act, or both compute and perform I/O? A useful reframing: one thing at the current level of abstraction. `placeOrder()` may call validate, charge, and persist — that's still one thing (placing an order) expressed as three sub-steps — as long as it does not also inline SQL strings or HTTP header parsing.

code

pseudocode · 18 lines
pseudocode
// Doing three things: validating, computing, and sending.
function handleOrder(order) {
  // validate
  if (order.items.isEmpty()) throw Error("empty")
  if (order.total < 0) throw Error("negative")
  // compute
  var tax = 0
  for (item in order.items) tax += item.price * rateFor(item.category)
  // notify
  smtp.connect(); smtp.send(order.email, "Total: " + (order.total + tax))
}

// One thing at one level of abstraction:
function placeOrder(order) {
  validate(order)
  let total = order.total + taxFor(order)
  notifyCustomer(order, total)
}

go deeper

for a junior

State the rule and give one concrete detection test (name needs "and", or section comments mark phases) plus the Extract Function fix.

for a middle

Tie it to level of abstraction and the extract test; show you know length is a symptom, not the rule; connect to testability (decision vs I/O).

for a senior

Frame it as SRP at function scale with the change-reason test; discuss over-extraction, cohesion as the counter-force, and orchestration functions being legitimately longer.

for a principal

Discuss how to make this operational at team scale: review heuristics over hard line limits, when the guideline conflicts with locality of behavior and debuggability, and how to avoid a codebase of thousands of two-line single-caller helpers.

## What the rule says "Functions should do one thing. They should do it well. They should do it only." This is the central function-design rule popularized by Robert C. Martin in *Clean Code*, but the underlying idea is older: it is the **Single Responsibility Principle (SRP)** applied at function granularity — a unit should have one reason to change. ## The problem: "one thing" is not self-defining Every function can be decomposed further. `add(a, b)` "does" a load, a load, an ALU op, a store. So a literal reading of the rule leads to infinite subdivision and is useless. The rule only becomes operational when you attach it to a **level of abstraction**. **Definition — level of abstraction:** how far a statement is from the machine and how close it is to the domain vocabulary. `total = price * quantity` is low-level. `applyLoyaltyDiscount(cart)` is high-level. A statement's level is roughly "how many concepts would I have to explain to a domain expert to justify this line?" So the working rule is: **a function does one thing when all of its statements are at one level of abstraction below the function's own name, and together they exactly constitute what the name promises — no more, no less.** ## Concrete detection tests 1. **The extract test (the most reliable one).** Try to pull a contiguous block out into a new function. If you can give that new function a name that *describes a different idea* rather than merely restating the parent's name (`processOrderPart2` is a restatement; `chargeCard` is a different idea), then the parent was doing more than one thing. Conversely, if the only honest name for the block is a paraphrase of the parent, you have hit bottom. 2. **The conjunction test.** Write the truthful name. If it needs `and`, `or`, `then`, or a vague verb (`handle`, `process`, `manage`, `doStuff`), the function is a bundle. `validateAndSaveUser` is two things wearing one name. 3. **The section-comment test.** Comments that announce phases (`// parse input`, `// then compute`, `// finally, notify`) are extraction markers. Each section wants to be a function whose name replaces the comment. 4. **The mixed-concerns test.** Watch for a function that simultaneously: makes a policy decision *and* performs I/O; formats output *and* computes a value; handles the happy path *and* implements retry/backoff; validates *and* mutates. 5. **The change-reason test (SRP proper).** Ask: what distinct events would force me to edit this function? "Tax rules change" and "we switch from SMTP to a queue" are two different reasons — two responsibilities living in one body. 6. **The blank-line test.** Blank lines inside a small function usually separate phases; phases usually want names. ## Why it matters (the actual payoff) - **Naming becomes possible.** A function that does one thing can be named precisely; the name then documents it, and the comment becomes unnecessary. - **Reuse.** Extracted sub-steps are reusable; embedded sections are not. - **Testability.** A function that only *decides* can be unit-tested with no test doubles. Once decision and I/O are fused, every test needs mocks or a database. - **Diff readability and merge behavior.** Small, single-purpose functions localize change; two teams editing two responsibilities in one body collide. - **Reading cost.** You can read a well-decomposed call chain top-down and stop as soon as you have enough detail. A 200-line function forces you to hold all of it in working memory at once. ## Trade-offs and edge cases - **Over-extraction is real.** Splitting until every function is two lines can produce a maze where understanding one behavior requires opening nine files. The counter-force is **cohesion**: sub-functions should be meaningful concepts in the domain, not arbitrary slices. If a helper is called from exactly one place, has a name that only makes sense in that caller's context, and carries five parameters to reconstruct the caller's state, extraction hurt more than it helped. - **Orchestration functions are legitimately "long-ish".** A top-level function that calls ten well-named steps in sequence does one thing (orchestrating) even at 15 lines. Length is a symptom, never the rule itself. - **Performance.** Call overhead is almost always irrelevant on modern runtimes (inlining, JIT). Only in genuinely hot inner loops with measurements in hand is inlining by hand justified — and then document it. - **Error handling is one thing.** A function whose body is `try { doWork() } catch (e) { translate(e) }` does one thing: error translation. Mixing business logic into the `try` block is the violation. ## The refactoring move The mechanical fix is **Extract Function** (Fowler): select the block, name it after *what it accomplishes*, not *how*; pass in what it reads; return what the caller needs. Repeat until each function's body is one level below its name. Then re-read the parent: it should now read like a short paragraph of prose.

  • Isn't "keep functions under N lines" the same rule, just easier to enforce?
    No. Line count is a proxy, not the rule. A 40-line function that calls eight named steps in sequence may do exactly one thing; a 6-line function that validates, mutates global state, and logs does three. Enforce the concept in review; use line-count linting only as a smell detector that opens the conversation.
  • How do you decide when to stop extracting?
    Stop when the only name you can give the extracted block is a paraphrase of its caller, when the helper needs many parameters just to rebuild the caller's context, or when the helper is not a nameable concept in the domain. Those signal you have gone below the useful level of abstraction.
  • How does 'do one thing' relate to the Single Responsibility Principle?
    It is SRP at function granularity: one reason to change. SRP is usually stated for classes/modules, but the same test — enumerate the distinct events that would force an edit — applies to a function body.

A recipe step reads "make the sauce", not "heat pan; mince 2 cloves; ...; whisk 90 seconds". "Make the sauce" is one thing at the recipe's level; its own sub-recipe lives one level down. Mixing whisking instructions into the top-level step list is the violation.

saying these in an interview costs you the question

  • "Do one thing means under 5 lines" — conflating a length proxy with the rule
  • Extracting arbitrary line ranges into helpers named step1/step2/processPart2
  • Claiming any function that calls other functions is doing multiple things
  • Ignoring that a function mixing a decision with I/O is the classic two-things case
  • Splitting until every helper needs 5 parameters to rebuild the caller's context, then calling it clean

context

open as a page

Why are boolean "flag" arguments considered a function-design smell, and what refactorings remove them? Also: what is the practical guidance on how many parameters a function should take?

level: juniorimportance: must knowfreq 72%

basics

~20 s

A boolean parameter means the function does two things — one per branch — and the call site save(user, true) is unreadable. Fix by splitting into two clearly named functions. Prefer 0-2 parameters; 3 is suspicious; 4+ usually means a missing object.

open as a page

Explain the "one level of abstraction per function" rule and the step-down (newspaper) ordering rule. How do you detect a mixed-abstraction function and fix it?

level: seniorimportance: must knowfreq 55%

basics

~20 s

Every statement in a function should sit at roughly the same conceptual distance from the domain — don't mix high-level policy with low-level string or byte fiddling. Step-down means each function is followed by the ones it calls, so the file reads top to bottom, general to detailed.

open as a page

What is Command-Query Separation (CQS) in function design, what concrete problems does it prevent, and where is it deliberately violated?

level: middleimportance: should knowfreq 58%

basics

~20 s

CQS says a function should either change state (a command, returning nothing) or return a value (a query, changing nothing) — never both. It keeps queries safe to call and read, so if (set(x)) style confusion disappears.

open as a page

What is a hidden side effect in a function, why is temporal coupling a particularly dangerous form of it, and how do you design the problem away?

level: seniorimportance: should knowfreq 48%

basics

~20 s

A hidden side effect is a change the function makes that its name doesn't advertise — mutating a global, a parameter, or session state inside something that looks like a check. It breaks callers' assumptions and creates order dependencies between calls that nothing enforces.

open as a page

"Extract till you drop" produces very small functions. As a technical leader, how do you decide when further decomposition stops paying off, and what team-level policy do you set instead of a line-count rule?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Stop when a helper isn't a nameable concept — when its name just paraphrases the caller, it has one caller, or it needs many parameters to rebuild the caller's context. Set a policy on naming, cohesion, and abstraction level in review; use line-count linting only as a smell trigger, never as a gate.

open as a page