Top-Down Design
5 min read
Core idea
Top-down design is the practice of describing a program at its highest level first — as a short list of named steps — and then recursively breaking each step down until every piece is small enough to implement directly. In shell, the recursion is expressed with functions: the main script reads as a sequence of function calls, each function is initially an empty stub, and the script remains runnable at every level of refinement. The work of "writing the script" becomes the work of replacing one stub at a time with real code.
Author's framing: This process of identifying the top-level steps and developing increasingly detailed views of those steps is called top-down design. It is well-suited to shell programming.
The main routine is the table of contents
The top of the script is a small, readable list of function calls — report_uptime, report_disk_space, report_home_space — that names every concern the program addresses. Anyone reading the file can understand what the program does in ten seconds without scrolling past the top section. The function bodies, defined below, contain the how. This separation turns the script into a document with both an outline and the content the outline points to.
Stubs let you test the spine before the muscles
A stub is a function definition with no real work in it — sometimes just a return, sometimes an echo that prints "function X ran" so you can see the call order. With stubs in place, the script runs even though it does almost nothing useful yet. You can confirm that every function is called in the right order, with the right surrounding text, before writing the first line of real logic. When a stub gets filled in, only the new code is suspect if the script suddenly breaks.
Local variables keep functions independent
Bash variables are global by default — assigning foo inside a function changes the script's foo outside the function. That tight coupling makes functions hard to compose. The local keyword scopes a variable to the function it's defined in, so the function can use natural names (count, result, total) without colliding with other functions or the caller. Functions written with local everywhere can be cut and pasted between scripts.
Functions inherit redirection from group commands
A function body is a group command — { ... } — and that group's stdin and stdout can be redirected as a unit. Calling report_uptime > out.html sends every echo inside the function to the file. Piping report_uptime | grep load filters the function's combined output. This is the mechanism that lets a top-level function produce a complete HTML document by simply concatenating the output of its children, with no per-function file handling code.
Why it matters
The reason to design top-down is that you cannot hold a 200-line linear script in your head but you can hold a six-line main routine in your head. By keeping the program flat at the top and deep at the bottom, every reader (including you, six weeks later) sees the structure first and the details only on demand.
It localises change
When the disk-space section needs a different command, you edit report_disk_space and nothing else. The main routine doesn't change. The other functions don't change. The diff is small, the test surface is small, and the risk of breaking unrelated output is small. A linear script with the same logic would require finding the right region of a wall of text and editing in place — every change is potentially everywhere.
It makes reuse cheap
A well-scoped function (one job, locals only, output to stdout) is portable. The book's example: if report_disk_space is useful at the prompt, drop the same function body into .bashrc under a shorter name. There is no surgery, no global state to extract, no caller-specific assumptions. This is the same property that makes functions composable inside a script: each one is a black box that produces output.
It surfaces missing logic before you write any code
The act of sketching the stubs forces you to enumerate every section the program needs. Gaps in the outline are obvious — there is no third stub between "open body" and "close body" — and you fill them with empty functions before you write any of them. The outline acts as a checklist that you cross off as you fill stubs in.
Key takeaways
Mental model
Practical application
-
Outline the program as bullet points before writing any shell. Each bullet becomes one function.
-
Write the main routine as a flat sequence of calls to function names that don't exist yet. Don't worry about implementations.
-
Define every function as a stub with
{ return; }or{ echo "ran: $FUNCNAME" >&2; }. Make the script executable and run it. The output should at least produce the right structure. -
Fill in stubs depth-first or breadth-first — your choice, but one at a time. After each fill-in, run the script again. The diff between two successive runs should be small and obvious.
-
Mark every internal variable
local. If a function needs to expose a value, write it to stdout and capture with command substitution at the call site — do not rely on a side-effect global. -
When a function exceeds ~30 lines, decompose it further. It's a sub-program; give it its own stubs and refine.
Example
Suppose you are writing a release-notes generator for your team. Top-down, the outline is:
- fetch the merged PRs since the last tag,
- group them by label (
feature,fix,chore), - render each group as a Markdown section,
- emit a header with the version and date.
You sit down and write main() as exactly four lines: fetch_prs; group_by_label; render_sections; emit_header. Then you write fetch_prs() { echo "TODO" >&2; } and three more like it. You run the script. It prints four TODOs in order — that's the spine, working. Now you fill in fetch_prs to actually call gh pr list. Run it again — the spine still works, and now fetch_prs emits real data. The next session you fill in group_by_label, then render_sections, then emit_header. Each session ends with the script doing strictly more than it did before. There is never a moment where the script is "in pieces on the floor" because the spine has been intact since hour one.
Related lessons
Related concepts
- Top-Down Designlinked concept
- Stepwise Refinementlinked concept
- Shell Functionslinked concept
- Modular Designlinked concept