Design, Don’t Just Code: Why Ousterhout’s Philosophy Still Matters
A deep dive into the timeless principles behind clean, scalable software design
1. Introduction: Why This Book Still Matters
In a world increasingly shaped by AI-generated code and lightning-fast development cycles, it's tempting to believe that good software design is becoming less relevant. After all, if tools like GitHub Copilot or GPT-4 can write working functions on command, do we still need to obsess over structure, naming, or abstraction depth?
John Ousterhout answers with a clear, experienced voice: Yes—more than ever.
A Philosophy of Software Design (2nd Edition) is not about algorithms or new programming languages. It’s about how to think about software—and more importantly, how to manage complexity so systems remain understandable, adaptable, and sustainable over time. Based on decades of industry experience (including creating Tcl and the Raft consensus algorithm) and years of teaching software design at Stanford (CS190), Ousterhout’s central thesis is blunt: “Complexity is the root cause of the majority of problems in software.”
This book is structured around identifying, preventing, and managing complexity. But Ousterhout’s contribution isn’t just in highlighting the problem—his lasting value is in proposing a coherent set of design philosophies that help developers write code that’s not just correct, but clear, simple, and maintainable.
At under 200 pages, it’s concise, opinionated, and laser-focused. There’s no filler. And unlike most software engineering texts that drown in jargon, this one speaks plainly and directly to working developers. Ousterhout distills ideas in a way that’s both academically informed and intensely practical—like advising readers to “design it twice” or “define errors out of existence.”
This review will explore:
The book’s major themes and most actionable principles;
The real-world utility of its design heuristics;
Its relevance in the modern development era dominated by tooling and AI;
And why—despite its simplicity—it remains one of the most valuable books a developer can read today.
Let’s dive in.
2. Complexity: The Root of the Problem
Ousterhout opens the book by asserting that complexity is the fundamental enemy of good software. This isn’t about performance bottlenecks or clever data structures. He defines complexity as anything that makes a system hard to understand or change. In his view, complexity is a design failure—and it's usually self-inflicted.
One of the most useful distinctions he draws is between strategic and tactical programming. Tactical programmers focus on getting something to work now. Strategic programmers focus on making the system easier to work with in the future. Most software issues, he argues, stem from teams operating tactically under the illusion of speed, only to be slowed later by the weight of accumulated complexity.
He provides concrete symptoms to look for:
Change amplification: making one change requires edits in many places.
Cognitive load: understanding one part of the system requires understanding many others.
Unknown unknowns: it’s not obvious what to consider when making a change.
These are not exotic edge cases—they're everyday realities in poorly structured systems. Ousterhout’s central point is that complexity compounds silently, and most of it can be designed away with foresight. This argument is both practical and deeply conservative: simple systems are safer, easier to maintain, and scale better over time—not necessarily in performance, but in human comprehension and changeability.
The real insight here isn’t that complexity is bad. That’s obvious. It’s that developers systematically underestimate its cost, and many common practices—like layering too many abstractions or overusing design patterns—introduce complexity rather than remove it.
In sum: complexity is inevitable, but accidental complexity is a choice. The rest of the book is about how to avoid making that choice.
3. Modules Should Be Deep
If Ousterhout has a single design commandment, it’s this:
“Modules should be deep.”
This principle is arguably the centerpiece of the book. It’s simple, but not simplistic—and it’s often ignored in real-world codebases.
A deep module hides a substantial amount of complexity behind a simple interface. It does a lot for the caller, but demands very little in return. A shallow module, by contrast, exposes too many details or does too little to justify its existence. It adds structure without reducing complexity—which, in Ousterhout’s view, is a net negative.
He illustrates this with examples like getters and setters, or wrapper classes that expose one object through another without adding any meaningful abstraction. These are shallow: they create surface structure but do nothing to help manage or encapsulate complexity.
“The best modules are those whose interfaces are much simpler than their implementations.”
This idea goes beyond code aesthetics—it’s about design leverage. A deep module allows you to make changes to the internals without affecting the rest of the system. It localizes complexity and reduces the burden on future developers (including your future self). Shallow modules, on the other hand, often act as liabilities: they’re brittle, hard to modify, and confusing to use.
He also connects this idea to general-purpose design: the more general a module is, the more scenarios it can handle without changes. Generality makes modules deeper—if done well.
This has clear implications:
Don’t write a class just to satisfy a textbook OO pattern.
Don’t create interfaces that pass the problem to the caller.
Look for modules that can absorb complexity so the rest of the system stays clean.
In short: a module’s value lies in how much complexity it removes from the rest of the codebase. The depth of a module is a measure of its contribution to simplicity.
4. Information Hiding and Layering
Building on the concept of deep modules, Ousterhout re-emphasizes one of software engineering’s oldest but most often violated principles: information hiding.
The idea is straightforward: modules should hide their implementation details from the rest of the system. But in practice, developers frequently leak information—through poorly designed interfaces, unnecessary pass-through methods, or exposing internal data structures that tie components together too tightly.
Ousterhout introduces the concept of leakage as a design smell. For instance, if an interface requires callers to understand how data is internally represented, or if changes to one module force changes to others, information is not truly hidden. The system becomes brittle.
He offers a powerful warning:
“Every piece of knowledge that another module needs about your module represents a failure in abstraction.”
One particularly insightful critique is directed at temporal decomposition—structuring a program based on the order of operations rather than responsibility. This leads to systems where the correctness of one module depends on another module having been called first. It violates both encapsulation and layering, and results in code that’s hard to reuse or reason about.
To combat this, Ousterhout advocates for layered architectures, where each layer provides a distinct abstraction and hides as much complexity as possible from the layers above. Importantly, he warns against layering for its own sake—structure without simplification is not design.
The goal is not just modularity, but modularity with asymmetric knowledge: each part of the system knows as little as possible about the rest.
In essence, information hiding is what makes depth possible. Without it, modules aren’t truly modular—they’re just distributed.
5. Pull Complexity Downward & Better Together or Apart?
A common misconception in software design is that complexity is something you distribute evenly across components. Ousterhout rejects this idea entirely. Instead, he introduces a principle with profound implications:
“Pull complexity downward.”
In other words, when faced with a choice of where to place complexity in a system, push it into the lower-level modules—the ones fewer people need to understand, and that are changed less frequently. Higher-level code should remain simple, intuitive, and easy to read. This makes the overall system more accessible and reduces the cognitive burden on most developers.
This principle is especially relevant in modern full-stack systems, where business logic, APIs, and infrastructure code often intertwine. Ousterhout’s advice is clear: hide complexity deep in the stack, so the upper layers stay clean and expressive.
Complementing this idea is a practical question developers constantly face:
Should these two things be combined—or kept separate?
In the chapter “Better Together or Better Apart?”, Ousterhout offers a nuanced heuristic. Systems should not be split merely for organizational tidiness. The real question is: how tightly are these elements coupled conceptually? If two pieces of functionality are always used together, or if separating them creates more indirection or shared knowledge, then separating them adds complexity instead of removing it.
His recommendation: only separate when it increases the clarity or reduces duplication. Otherwise, err on the side of cohesion. Over-separation—often done in the name of purity or pattern adherence—can lead to “class explosion,” a design that’s technically modular but practically unreadable.
These two principles—pulling complexity downward and merging things that are better together—reveal a core philosophy:
Design isn’t about enforcing rules. It’s about making systems simpler to understand and easier to change.
6. Design It Twice & Define Errors Out of Existence
Software design is often framed as a one-shot process: requirements in, design out. But Ousterhout challenges this mindset with a blunt and valuable recommendation:
“The first design that comes to mind is usually not the best. Design it twice.”
This chapter stands out because it addresses a common failure mode: settling. Developers often go with the first working design that satisfies the requirements, assuming optimization or refactoring can come later. But Ousterhout argues that stepping back—even briefly—and exploring an alternative design often leads to better outcomes.
He doesn’t prescribe weeks of design paralysis. Instead, he advocates for lightweight iteration—sketch two approaches, weigh tradeoffs, and choose the one that results in simpler, more robust code. Even ten minutes of intentional second-pass thinking can prevent hours of debugging or future rework.
This connects closely to another of his boldest principles:
“Define errors out of existence.”
Rather than designing systems that detect and handle a wide variety of failure cases, Ousterhout pushes developers to rethink interfaces and behavior to avoid errors altogether. If callers must remember to follow complex rules to avoid misuse, that’s a failure in design—not user error.
He illustrates this with examples like the Java File
class, which forces the caller to check for existence, readability, writability, and file type before performing operations—making every usage site error-prone. A better design might encapsulate those checks and provide clear, intention-revealing methods like openForWrite()
that either succeed or fail clearly.
This principle reframes error-handling as a design challenge, not just a coding one. The fewer error conditions your system exposes, the more reliable and maintainable it becomes.
Taken together, “Design it twice” and “Define errors out of existence” are powerful reminders:
Design isn’t about perfecting complexity—it’s about eliminating it before it shows up in code.
7. Comments, Naming, and Writing for Humans
Midway through the book, Ousterhout takes a firm stance on one of the most debated topics in software development: comments. His position is direct, even unfashionable in some circles:
“Comments are not optional. They are an integral part of good design.”
He starts by dismantling the four most common excuses developers use to avoid writing comments:
“The code is self-documenting.”
“Comments get out of date.”
“Good naming is enough.”
“Comments take too much time.”
To each, he responds with examples and counterpoints. The core argument is that comments capture design intent—the why behind the what. Code tells you what it does. Comments should tell you why it’s written that way, what tradeoffs were considered, and what invariants must be maintained. Without this context, even the cleanest code can mislead.
One of his most practical recommendations:
“Write the comments first.”
By forcing yourself to explain what a module or method is supposed to do before writing it, you clarify your design intent up front—and often spot flaws in your planned structure. This is a lightweight design process in disguise.
Complementing his stance on comments is a deep concern with naming. Ousterhout argues that naming is not cosmetic—it’s a central act of design. Good names shape how developers think about a system. Bad names obscure intent and invite misuse.
He offers a few guiding principles:
Names should reflect what something does, not how it does it.
Avoid generic words (“handle,” “data,” “manager”) that add no meaning.
Be consistent: similar concepts should have similar naming schemes.
This section underscores a central thesis of the book:
Code is written for humans first, computers second.
Naming, comments, and structure aren’t documentation after the fact—they are the design.
8. Consistency, Obviousness, and Designing for Performance
As the book nears its conclusion, Ousterhout shifts from specific techniques to broader design behaviors. Three principles stand out here: consistency, obviousness, and a measured approach to performance.
Consistency:
“A system is easier to learn and use if it behaves the same way in all situations.”
Ousterhout argues that consistency reduces cognitive load. When a developer encounters one part of your system, they should be able to predict how other parts behave. This applies to naming, API shape, error handling, and even file organization. Every inconsistency forces readers to stop and ask, “Why is this different?”—which slows them down and increases risk.
He cautions against local optimization—tweaking small parts for readability or cleverness if it undermines a broader pattern. Design isn’t just about making each module good in isolation. It’s about building systems that feel coherent as a whole.
Obviousness:
In Ousterhout’s view, the gold standard for software is not cleverness—it’s clarity.
“The most important goal of any piece of code is that it be obvious.”
This doesn’t mean oversimplified. It means that someone reading the code should be able to understand what it does and why, with minimal digging. Ousterhout encourages writing exemplary code—code that makes good practices easy to follow and bad practices harder to introduce.
He sees “obviousness” not as a vague quality but a design constraint: something you intentionally design for, test for, and prioritize over technical elegance.
Designing for Performance:
Ousterhout urges developers to be pragmatic and data-driven when it comes to optimization.
“The best way to improve performance is to change the design.”
Rather than hand-tuning code blindly, he recommends:
Measure first. Use profiling tools to find real bottlenecks.
Design second. Step back and assess whether the performance problem stems from a poor abstraction or module boundary.
Only then optimize. And even then, do so without introducing unnecessary complexity.
Performance isn’t an excuse for bad design—it’s a reason to improve design. Fast code that’s hard to read is a short-term win and a long-term liability.
9. Relevance Today and Final Verdict
With AI-assisted coding tools like GitHub Copilot and GPT-4 becoming increasingly capable, it’s fair to ask: does software design still matter?
John Ousterhout’s A Philosophy of Software Design answers that with a resounding yes—and arguably makes a stronger case now than when it was first published. In an era where generating code is easier than ever, the real bottleneck isn’t typing code, it’s understanding, modifying, and extending it over time.
The core message of the book—that complexity is the silent killer of software projects—has only grown more relevant. AI can write functions, but it can’t (yet) assess system-level clarity, anticipate long-term change patterns, or enforce a consistent abstraction strategy. Those are human design decisions. And they are exactly what this book teaches.
More than anything, A Philosophy of Software Design provides a mental framework. It doesn’t teach patterns. It teaches judgment. It trains developers to see complexity before it accumulates, to value simplicity not as minimalism, but as leverage. It argues for thinking not just in lines of code, but in systems that evolve.
Final Verdict:
Audience: Intermediate-to-senior developers, tech leads, and product-minded engineers.
Length: Under 200 pages, easy to revisit.
Style: Clear, direct, opinionated.
Biggest strength: A coherent, actionable philosophy of design.
Limitation: Doesn’t cover systems at scale (e.g. distributed architecture, CI/CD, etc.), but it’s not meant to.
Verdict: A modern classic. If you care about writing software that lasts, this is essential reading.
Related Books
The Pragmatic Programmer: Your Journey to Mastery, David Thomas et al. 2019