skip to content

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%

answer

  1. One level below the function's own name
  2. Domain nouns next to charAt/bit-ops = mixed levels
  3. Newspaper: headline → lede → details
  4. Each function followed by its callees (step-down)
  5. Extraction needing 5 params = wrong seam

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.

solid answer

~50 s

**One level of abstraction per function:** all statements in a body should be one step below the function's name. Mixing `calculateTax(order)` (domain-level) with `sb.append(String.format("%,.2f", x))` (formatting-level) in the same body forces the reader to context-switch and hides which lines are essential detail versus incidental. Detection heuristics: domain nouns sitting beside primitives/loops/regex; a mix of long descriptive calls and terse operators; nested conditionals inside otherwise declarative code; comments that translate low-level lines back into domain terms. The fix is Extract Function on the low-level block, named at the higher level, so the caller reads as policy and the callee holds mechanism. **The step-down rule** (Martin's "newspaper metaphor") governs ordering: put the highest-level function first, then each function it calls, recursively — so reading downward descends one abstraction level at a time and a reader can stop as soon as they have enough detail. Together they let you read a file like an article: headline, lede, then details.

code

pseudocode · 18 lines
pseudocode
// Mixed levels: domain policy next to character-level formatting.
function renderInvoice(invoice): String {
  let total = invoice.lines.sum { it.qty * it.unitPrice }
  let taxed = total * (1 + taxRateFor(invoice.region))
  var s = ""
  let cents = round(taxed * 100)
  var digits = cents.toString()
  while (digits.length < 3) digits = "0" + digits            // <- level drop
  s += digits.dropLast(2) + "." + digits.takeLast(2) + " EUR" // <- level drop
  return header(invoice) + s
}

// One level per function, arranged step-down.
function renderInvoice(invoice): String =
  header(invoice) + formatMoney(totalWithTax(invoice))

function totalWithTax(invoice): Money = ...   // next level down
function formatMoney(amount): String  = ...   // next level down

go deeper

for a junior

Define both rules plainly — same conceptual level inside a function; callees below callers in the file — and give one mixed-level example plus the Extract Function fix.

for a middle

Add detection heuristics (domain nouns beside primitives, explanatory comments, nesting depth) and explain why mixing costs the reader working memory and blocks reuse.

for a senior

Discuss ordering-convention conflicts, shared helpers and recursion breaking strict ordering, error handling as its own level, and the extraction stopping criteria (parameter explosion, single-caller context-bound helpers).

for a principal

Weigh decomposition against locality of behaviour and debuggability, set a team-wide ordering convention consistent with tooling, and treat step-down as a code-review reading aid rather than a lint rule to enforce with churn.

## Rule 1 — One level of abstraction per function **Level of abstraction** = the conceptual distance between a statement and the machine. A rough ladder: | Level | Example statement | |---|---| | Domain/policy | `applyLoyaltyDiscount(cart)` | | Application step | `repository.save(order)` | | Data manipulation | `total = items.sum { it.price }` | | Primitive/mechanism | `buf[i++] = (char)(c & 0x7F)` | The rule says: within one function body, all statements should sit at **one level below the function's own name**. If the function is named at the policy level, its body should read as application steps — not as byte twiddling. ### Why mixing hurts 1. **Reader context-switching.** Understanding a mixed function means repeatedly zooming between "what business rule is this?" and "why is there a `-1` here?". Working memory is the scarce resource in code reading. 2. **You can't tell essence from accident.** In a uniform-level function, every line is essential to the story. In a mixed one, three lines are the story and twelve are incidental encoding details, and nothing marks which is which. 3. **Detail is sticky.** Once a low-level detail sits in a high-level function, later maintainers add sibling details next to it, and the function accretes into a 200-liner. 4. **It blocks reuse and substitution.** Low-level mechanisms buried inline can't be swapped, tested, or reused. Extracted, they become a seam (e.g. replace an inline formatting loop with a formatter you can test or localize). 5. **It defeats naming.** A mixed function can't be named at one level — you end up with `processAndFormatAndPersistOrder` or, worse, `process`. ### Detection heuristics (usable in review) - **Vocabulary mixing:** domain nouns (`invoice`, `entitlement`) in the same body as `charAt`, `substring`, `%`, `<<`, index arithmetic, or manual null-checks. - **Line-length variance:** long, descriptive method calls interleaved with terse symbolic lines. - **Explanatory comments:** a comment restating a low-level line in domain terms (`// this trims the currency symbol`) is a name waiting to be a function. - **Nesting depth:** a nested loop or 3-deep conditional inside otherwise declarative code is almost always a lower level trying to be extracted. - **Error handling mixed with logic:** `try/catch` around a domain algorithm mixes the mechanism of failure translation with the policy. - **A `switch`/long `if-else` chain in a high-level function:** usually a type-based dispatch that belongs behind polymorphism or a table, one level down. ### The fix **Extract Function**, naming the extracted block *at the caller's level* — describing **what** it accomplishes, never **how**. `formatCurrency(amount)` rather than `appendDigitsWithCommas(sb, x)`. Repeat until each body is homogeneous. In *Clean Code* this is stated as "extract till you drop" — though see the caveats in the trade-off section below. ## Rule 2 — The step-down rule (newspaper metaphor) A newspaper article starts with a headline, then a paragraph summarizing the story, then increasing detail; you stop reading when you know enough. Code should be arranged the same way: > **Every function should be followed by those at the next level of abstraction, so the program reads top-down, one level at a time.** Concretely: the public entry point sits at the top of the file; the functions it calls come next in call order; their callees follow. Reading downward is a depth-controlled descent. The reader can absorb the top-level story in 10 lines and stop, or continue for detail. ### Consequences and practical notes - **It makes a file scannable without a call hierarchy tool.** Especially valuable in code review UIs and diffs, where IDE navigation isn't available. - **It conflicts with the alternative convention** of grouping by visibility (all public methods, then all private) or alphabetically. Step-down ordering deliberately interleaves visibility to preserve narrative order. Pick one convention per codebase and enforce it, because mixed conventions are worse than either. - **Language constraints matter.** Languages requiring declaration before use (classic C, without forward declarations) fight step-down ordering — a common practical exception. Most modern languages don't care. - **Recursion and shared helpers break perfect ordering.** A helper called from three levels lands somewhere arbitrary; convention is to put widely shared helpers at the bottom. - **Diff churn:** strict reordering on every change creates noisy diffs; most teams apply step-down to new code and don't reshuffle stable files just for ordering. ## The trade-offs (senior/principal territory) - **Over-decomposition.** Extracting until every function is two lines can turn a readable 30-line algorithm into a scavenger hunt across 12 single-caller helpers. The reader now pays *navigation* cost instead of *reading* cost. The right stopping point is where each function is a nameable concept, not a slice. - **Locality of behaviour.** There's a legitimate counter-school ("locality of behaviour"): code you must understand together should be visible together. Extracting a block that is only ever meaningful inside its caller trades a small reading win for a real navigation cost. - **Parameter explosion is the tell.** If extraction requires threading five parameters (or a mutable accumulator) into the helper, the block was not an independent concept — undo it. - **Debuggability.** Deep chains of tiny functions produce long stack traces and more step-into during debugging. Usually a fair price; occasionally not, in tight code you debug often. - **Performance** is essentially a non-issue on JIT/optimizing runtimes; raise it only with measurements. ## How the two rules combine One-level-per-function decides **what goes in a function**; step-down decides **where the function goes**. Applied together, a file becomes a tree flattened in pre-order: the top reads as the executive summary in domain language, and each successive tier answers "how?" for the tier above. That is the concrete meaning of "code should read like prose".

  • Step-down ordering conflicts with the common convention of putting all public members first and privates last. How do you decide?
    Pick one per codebase and enforce it mechanically; the cost of inconsistency exceeds the benefit of either. Step-down favours reading a file as a narrative and works well for behaviour-heavy classes; public-then-private favours API scanning and matches many language style guides and formatters. If your tooling auto-sorts members, fighting it is not worth the diff churn.
  • When does extracting one more function make the code worse?
    When the extracted block is not a nameable domain concept, when it has exactly one caller and only makes sense in that caller's context, when it needs many parameters or a mutable accumulator to be pulled out, or when the resulting name is a paraphrase of the caller. Those are signs you crossed from decomposition into fragmentation, trading reading cost for navigation cost.
  • How does the one-level rule interact with error handling?
    Error handling is itself one thing. Wrapping a domain algorithm in try/catch inside the same function mixes failure-translation mechanism with policy. The idiomatic shape is a thin function whose whole body is the try/catch, delegating the work to a function that only expresses the algorithm — keeping each at a single level.

A newspaper article: headline, then a one-paragraph summary, then progressively finer detail. You stop reading at the depth you need. A file ordered by the step-down rule works the same way; a mixed-abstraction function is a headline with a footnote about the printing press wedged into the middle of it.

saying these in an interview costs you the question

  • Treating "one level of abstraction" as "one statement per function"
  • Reordering entire stable files for step-down purity, creating huge noisy diffs
  • Extracting a block into a helper that needs five parameters and a mutable accumulator, then calling it cleaner
  • Naming extracted functions after how they work rather than what they accomplish
  • Assuming extraction always wins, ignoring navigation cost and locality of behaviour

context