A Philosophy of Software Design — Distilled Guide
Source: John Ousterhout, A Philosophy of Software Design Central thesis: The core challenge of software design is managing complexity.
I. Complexity: Know the Enemy
Definition
Complexity is anything related to the structure of a software system that makes it hard to understand and modify. Complexity is not the same as system size — a small system can be complex, and a large well-designed system can be manageable.
Three Symptoms of Complexity
- Change Amplification: A seemingly simple change requires code modifications in many different places.
- Cognitive Load: Developers must absorb a large amount of information to complete a task safely. Note: fewer lines of code ≠ simpler — sometimes more code is actually simpler because it reduces cognitive load.
- Unknown Unknowns: It's unclear which code must be modified or what information is needed to complete a task. This is the most dangerous symptom.
Two Root Causes
- Dependencies: A piece of code cannot be understood or modified in isolation; it relates to other code that must also be considered.
- Obscurity: Important information is not obvious — vague names, missing docs, implicit conventions, hidden constraints.
Key Insight
- Complexity is incremental: It's not caused by a single catastrophic error; it accumulates through thousands of small decisions.
- Therefore you must adopt a zero-tolerance mindset — every bit of "minor" complexity matters.
II. Strategic vs. Tactical Programming
Tactical Programming (Anti-pattern)
- Goal: get features working as quickly as possible.
- Mindset: "Just make it work", "We'll refactor later."
- Result: complexity accumulates fast, tech debt spirals out of control.
Strategic Programming (Recommended)
- Goal: produce great design; working code is a byproduct.
- Mindset: invest roughly 10–20% of development time in design improvements.
- Practices:
- Look for opportunities to improve design with every change.
- Working code is not enough — design quality matters equally.
- The increments of software development should be abstractions, not features.
III. Deep Modules: The Most Important Design Concept
Core Metaphor
Think of a module as a rectangle:
- Width = complexity of its interface
- Height/Depth = amount of functionality hidden inside
Deep module: simple interface, rich implementation. (Good design) Shallow module: complex interface, does very little. (Bad design 🚩)
Classic Examples
- Deep: Unix file I/O — just 5 syscalls (open, read, write, lseek, close) expose a powerful file system.
- Shallow: Java I/O — reading a file requires composing FileInputStream, BufferedInputStream, ObjectInputStream, etc.
Practical Principles
- Design interfaces so the most common usage is as simple as possible.
- A simple interface matters more than a simple implementation.
- Rare use cases can accept more complex calling patterns, but the common path should never pay for them.
IV. Information Hiding and Information Leakage
Information Hiding
- Each module should encapsulate design decisions (knowledge), exposing only a simplified interface.
- Hidden information includes: data structures, algorithms, low-level mechanisms, policy decisions.
- Information hiding minimizes inter-module dependencies.
Information Leakage 🚩 Red Flag
- When the same design decision is reflected in multiple modules, information has leaked.
- Temporal decomposition is a common source: splitting modules by execution order (rather than by information hiding) causes steps to share excessive knowledge.
Fixing Leakage
- Merge shared knowledge into a single module.
- If merging isn't possible, unify the shared information behind a single deep module.
V. General-Purpose vs. Special-Purpose Modules
Core Principle: General-purpose modules are usually deeper.
- A general interface is simpler than a specialized one because it covers more use cases with fewer methods.
- When designing a new module, ask: What is the most general-purpose interface that can satisfy my current needs?
Judgment Criteria
- The interface should be general enough to support multiple use cases without modification.
- But the implementation can do only what's currently needed (don't over-build).
- General-purpose and special-purpose code should be cleanly separated.
VI. Different Layers, Different Abstractions
Principle
- A software system has multiple layers; each layer should provide a different abstraction from its adjacent layers.
- If two layers have similar abstractions, the layering is wrong.
Pass-Through Method 🚩 Red Flag
- A method that does almost nothing except forward its arguments to another method with a similar signature.
- This signals that the layers don't offer different abstractions — the responsibility split is flawed.
Pass-Through Variable 🚩 Red Flag
- A variable threaded from top to bottom through layers that don't use it.
- Solutions: context objects, dependency injection, or rethinking module boundaries.
VII. Pull Complexity Downward
Core Principle
- When complexity is unavoidable, the module should absorb it internally rather than pushing it to callers.
- Most modules have more users than developers — it's better for developers to suffer than for every user to suffer.
Anti-patterns
- Turning hard decisions into configuration parameters and pushing them to sysadmins.
- Throwing exceptions for uncertain conditions and letting callers handle them.
- These save effort in the short term but amplify complexity system-wide.
VIII. Define Errors Out of Existence
Core Insight
- Exception handling is one of the biggest sources of complexity.
- Reducing the number of exceptions that must be handled is one of the best techniques for reducing complexity.
Strategies
-
Redefine semantics so the error condition cannot arise.
- Example:
unset(key)succeeds even if the key doesn't exist — it simply guarantees "after the call, the key does not exist." - Example:
substring(start, end)auto-clips out-of-bounds parameters instead of throwing.
- Example:
-
Exception Masking: Detect and handle exceptions at a low level so they never reach callers.
-
Exception Aggregation: Handle multiple exception types in one centralized place instead of scattering handlers at every call site.
Important Distinction
- This is not about ignoring errors — it's about designing better semantics so error conditions simply aren't errors.
- Errors that truly require reporting (e.g., lost network packets) must still be handled properly.
IX. Design It Twice
- For any important design decision, conceive at least two different approaches before choosing.
- Even if the first idea seems great, force yourself to think of an alternative.
- Comparison dimensions: interface simplicity, generality, performance, implementation difficulty.
- This habit significantly improves design quality.
X. The Philosophy of Comments
Why Write Comments
- Comments capture design decisions and intent that code cannot express.
- Comments are part of the abstraction — good interface docs mean users don't have to read the implementation.
- Writing comments early exposes design problems before you invest in code.
- Good comments dramatically reduce cognitive load.
What Comments Should Describe
- Non-obvious information: the why, constraints, boundary conditions, side effects — things you can't see in the code.
- Comments should NOT repeat what the code already says. 🚩 Red Flag: Comment Repeats Code
Comment Layers
- Interface comments: describe what and why — no implementation details.
- Implementation comments: explain how and why this approach — why the code