A Philosophy of Software Design, 2nd Edition - Summary Notes

Author: John Ousterhout

Published: Jul, 2021

Last Modified: 11 Aug, 2025

The main argument of the book is that the most fundamental problem in software design is complexity, and that the key to managing complexity is to create systems that are simple and modular.

The True Nature of Complexity

So, what exactly is complexity? Anything related to the structure of a software system that makes it hard to understand and modify. It's not about the size or functionality of a system, but its ease of interaction. Complexity manifests in three key ways:

  • Change Amplification: A small change requires modifications in many different places.
  • Cognitive Load: Developers need to know a lot of information to complete a task, increasing the risk of bugs.
  • Unknown unknowns: It's unclear which code needs changing, or what information is necessary to make a successful modification.

Strategic vs. Tactical: The Mindset Shift

A key theme in the book is the distinction between strategic and tactical thinking in software design.

  • Tactical thinking: Focuses on immediate problems and solutions, often leading to quick fixes that can increase complexity over time.
  • Strategic thinking: On the other hand, involves considering the long-term implications of design decisions and prioritizing simplicity and modularity.
By adopting a strategic mindset, developers can create systems that are easier to understand, modify, and maintain. This slows you down initially but speeds you up significantly in the long run.

Working code isn't enough; your primary goal must be to produce a great design that also happens to work.

Cultivating Deep Modules and Hiding Information

A cornerstone of fighting complexity is modular design, which divides a system into relatively independent modules like classes or services. Each module has an interface (what users need to know to use it) and an implementation (how it works internally).

  • Deep Modules: The best modules are "deep" – they offer powerful functionality hidden behind a simple interface. The Unix I/O system calls are a prime example, providing vast functionality with only five basic calls.
  • Shallow Modules: These have complex interfaces for little functionality, offering minimal leverage against complexity. Ousterhout criticizes "classitis" – the mistaken belief that more, smaller classes are always better, which often leads to an explosion of shallow modules and system-level complexity
  • Information Hiding: This technique is crucial for creating deep modules. It encapsulates design decisions within a module's implementation, making them invisible to other modules. The opposite, information leakage, where a design decision is reflected in multiple modules, is a major "red flag".
The general-purpose modules are deeper and lead to better information hiding. Instead of tailoring modules to specific uses (which can cause "information leakage" and tight coupling), design interfaces to be general enough for multiple uses, pushing specialization upwards or downwards in the software stack.

If adjacent layers in a system have similar abstractions, it's a "red flag". This often manifests as "pass-through methods" (methods that just call another method with the same signature) or "pass-through variables" (variables passed through many intermediate methods that don't use them). Each layer should contribute significant, distinct functionality.

Defining Errors Out of Existence and Other Practical Tips

There are several strategies to reduce complexity and improve software design:

  • Define Errors Out of Existence: Exceptions are a major source of complexity. The best approach is to design APIs so that exceptional conditions become normal behaviors. For example, the Unix file deletion model, which allows a file to remain open after deletion until all processes close it, avoids errors that Windows' approach creates. Similarly, the Java substring method could be improved by automatically handling out-of-range indices rather than throwing exceptions.
  • Mask Exceptions: Handle exceptions at a low level so higher levels are unaware, like TCP retransmitting lost packets.
  • Aggregate Exceptions: Use a single handler for many exceptions, allowing them to propagate up to a top-level dispatcher.
  • Design it Twice: Don't settle for your first idea. Explore multiple radically different design options for a module or system, then compare their pros and cons. This iterative process leads to better designs and hones your design skills.
  • Write Comments First: Don't treat comments as an afterthought. Write them as part of the design process. If a comment is hard to write, the underlying design might be flawed. Comments should describe things not obvious from the code itself, such as rationale, high-level behavior, units, or invariants
  • Choosing Names: Good, precise, and consistent names create a clear image of the entity in the reader's mind, reducing ambiguity and bugs. Vague names are a "red flag".
  • Code Should Be Obvious: The ultimate goal is for code to be easily understood at a glance. Good naming, consistency, and judicious use of whitespace all contribute to this.
  • Designing for Performance: Simplicity and clean design often lead to faster systems. When performance is critical, design around the critical path (smallest amount of code needed to execute the desired task).
  • Decide What Matters: Structure your system around what truly matters, emphasizing important aspects and hiding less important ones. This principle underlies all good design decisions.

The Takeaway

The software design philosophy boils down to a fundamental belief: investing in good design is not a luxury; it's a necessity that pays dividends in the long run. By adopting a strategic mindset, focusing on deep, general-purpose modules, practicing information hiding, meticulously crafting comments, and choosing precise names, we can build software systems that are not only powerful and efficient but also a pleasure to work with.