skip to content

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