Engineering Atlas › Low-Level Design

Clean code habits for design interviews

14 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

Who clean code is for

Code is read many more times than it is written: in review, when debugging at 2 a.m., when someone extends it next year, and now when an agent reads it to make a change. Each reader has to rebuild your mental model from the text alone. Clean code minimises that rebuild cost. Every rule below is a way to put more of the model into the code and less into the reader's head.

Names

Names are the cheapest documentation and the one most often read.

Functions

A function is clean when a reader can predict what it does from its name and signature, and confirm it by reading the body once.

How long is too long? Length is a symptom, not the disease. A 40-line function that reads top to bottom as one sequence is fine; a 12-line function with four levels of nesting is not. A better test, from Martin Fowler: if you have to read the body to know what a block does, extract it and name it after its intention.

Errors and side effects

Comments

Write comments for what the code cannot say: why this approach, which constraint forced it, what was tried and failed, links to the incident or spec. Do not narrate the code (# increment i). When you need a comment to explain what a block does, extract the block into a function whose name is that comment. Keep public API documentation accurate; a wrong comment is worse than none.

Structure and data

Consistency and tools

Formatting arguments are a waste of review time. Use the formatter your ecosystem standardised on (gofmt, black or ruff format, prettier) and run it in CI. Use a linter for mechanical rules (unused variables, shadowing, complexity limits) so reviewers spend their attention on names and design. In an existing codebase, local consistency beats your preferred style: a reader who learns one pattern should find it everywhere.

Clean code with coding agents

Agents produce code that looks clean at a glance and fails on substance: long defensive blocks that catch every exception, comments restating each line, generic names like data, result and handle, and new helper functions duplicating ones that already exist two files away. They also follow what they see. A codebase with clear names, small functions and a lint config teaches the agent the house style; a messy one gets messier faster. Put your conventions in the repository (lint config, a short style note), and review generated code for names, error handling and duplication rather than formatting.

The same shape elsewhere

Clean code is the software version of plain-language writing rules used in law, medicine and aviation: short sentences, one idea per sentence, specific nouns, consistent terms, and the important thing first. A checklist in a cockpit and a clean function share a design goal: a tired reader under pressure must not misread it. The practice task builds the mechanical half of that, a small linter that flags long functions, deep nesting, long parameter lists, flag arguments and cryptic names.

Down the stack

Layer 1The contract

Clean code makes promises to the reader that you can check. Names match behaviour: a query does not mutate, a function named save persists. A function's signature lists everything it depends on that can vary (no hidden clock or global). Errors are explicit and specific. Mechanical limits hold across the codebase, for example at most 4 parameters, nesting at most 3, functions at most 50 statements. Those limits are proxies, but they are testable in CI, which turns "write clean code" from advice into a gate.

Layer 2The mechanism

Linters parse source into an abstract syntax tree and walk it. Cyclomatic complexity, from Thomas McCabe's 1976 paper, counts independent paths through a function's control-flow graph: edges minus nodes plus 2, in practice one plus the number of decision points (if, loop, case, &&, ||). Cognitive complexity, defined by SonarSource, adds a penalty for each nesting level, which matches how hard code feels to read better than raw path counts. Tools set defaults: Pylint flags more than 5 arguments (too-many-arguments), more than 50 statements and more than 5 nested blocks; ESLint's max-params defaults to 3 and max-depth to 4; gocyclo and Ruff's C901 report McCabe complexity above a threshold you pick. Formatters such as gofmt parse to an AST and print it back in one canonical layout, which is why they can never disagree with themselves and why diffs only show real changes.

Layer 3The machine

Clean code is mostly free at runtime. Small functions get inlined: Go's compiler inlines functions under a cost budget of 80 AST nodes, the JVM's JIT and V8's TurboFan inline hot small methods, so extracting a helper usually costs nothing after compilation. CPython does not inline: a Python function call costs roughly 50 to 100 ns, which matters only in loops running millions of times, where you would optimise deliberately anyway. The machine that clean code really optimises for is the reader's brain. Working memory holds about four chunks at once, so a function that needs the reader to track six variables, three nesting levels and a flag overflows it, and the reader starts scrolling back. Code review has a similar limit: reviewers find defects well in about 200 to 400 lines at a sitting and miss them beyond that, so smaller, clearer changes get better reviews.

Why it's built this way

  1. Code lives for years and is changed by people (and agents) who did not write it, so the cost that dominates is reading, not typing.
  2. Readers can hold only a few things in mind at once, so functions are kept small and shallow, with few parameters.
  3. Most of what a reader needs is in names and signatures, so naming and explicit dependencies get the most care.
  4. Hidden failures and side effects cause the most expensive bugs, so errors and I/O are made explicit and pushed to the edges.
  5. Humans argue about style forever, so the mechanical part is handed to formatters and linters, leaving review for judgement.

Diagram

flowchart TD
  W["Write or generate code"] --> F["Formatter (gofmt, black, prettier)"]
  F --> L{"Linter limits pass?"}
  L -- "too long, deep, many params" --> R["Extract function, guard clauses, value object"]
  R --> F
  L -- "yes" --> T{"Tests pass?"}
  T -- "no" --> W
  T -- "yes" --> V["Human review: names, errors, boundaries"]
  V -- "unclear intent" --> R
  V -- "approved" --> M["Merge"]

Worked example

A booking service has this function, written under deadline pressure:

def proc(d, items, rate, region, dry_run=False):
    total = 0
    for it in items:
        if it.qty > 0:
            if it.sku in d:
                p = d[it.sku]
                if region == "EU":
                    p = p * (1 + rate)
                total += p * it.qty
    if not dry_run:
        db.save_invoice(total)
    return total

Problems: the name says nothing; d, p and it are cryptic at this scope; five parameters; a flag argument that makes the function both compute and persist; nesting four deep; unknown SKUs are silently skipped, so a typo produces an invoice that is too small; and money is a float. Rewritten:

def invoice_total(lines, prices, tax_rate_bps):
    total_cents = 0
    for line in lines:
        if line.quantity <= 0:
            continue
        if line.sku not in prices:
            raise UnknownSku(line.sku)
        total_cents += prices[line.sku] * line.quantity
    return total_cents + total_cents * tax_rate_bps // 10_000

def issue_invoice(lines, prices, region, invoices):
    total = invoice_total(lines, prices, tax_rate_for(region))
    invoices.save(total)
    return total

invoice_total is pure, takes three parameters, uses integer cents and basis points, and fails loudly on an unknown SKU. The region rule moved into tax_rate_for, so adding a second taxed region is one table entry. The dry run is now simply calling invoice_total without issue_invoice. Nesting dropped from four to two. Performance did not change in any way that matters: the extra call costs nanoseconds against a database write of about 2 ms. The bug class "typo yields a cheap invoice" is gone.

Trade-offs

OptionStrengthsWeaknessesChoose when
Many small functionsEach is named, testable, easy to reuseReaders jump between definitions; names must be goodLogic has distinct steps worth naming
One longer linear functionReads top to bottom, no jumpingHarder to test parts; grows into a messA short script or a straight sequence with no reuse
Strict lint limits in CIConsistent; frees review timeRules fire on reasonable code; people add suppressionsLarger teams and agent-generated code
Guidelines onlyFlexible, no false positivesDrift; arguments in reviewSmall, senior teams
Comments explaining whyCaptures context code cannotRots when code changesNon-obvious constraints, workarounds, links to incidents
Self-describing code instead of commentsCannot go staleCannot explain whyExplaining what the code does

Pitfalls in production

Leadership lens

Check yourself

  1. Why is send_email(user, True) a problem, and what would you do instead?

    Show answer

    The boolean is meaningless at the call site and means the function does two things. Split it into two well-named functions, or use a named enum if there are more than two modes.

  2. When is a one-letter variable name acceptable?

    Show answer

    When its scope is a few lines and its meaning is conventional, such as a loop index or the parameter of a short lambda; name length should grow with scope.

  3. A function is 15 lines but has four levels of nesting. Is it clean by length? What would you change?

    Show answer

    Length is fine but nesting is not: use guard clauses for the edge cases and extract the inner loop body into a named function, so the main path reads at one or two levels.

  4. Does extracting many small functions slow code down?

    Show answer

    Rarely. Compiled languages and JITs inline small functions, and even in CPython a call costs tens of nanoseconds, which matters only in hot loops that you would profile and optimise deliberately.

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →