I recently read A Philosophy of Software Design, 2nd Edition by John Ousterhout. This is my review and summary of the book.

My review

This is a strong, highly readable book that every software developer can benefit from. It distills timeless principles about reducing complexity and making code easier to understand and modify. The ideas are simple, memorable, and broadly applicable. They’re valuable no matter what language or tech stack you use.

The good stuff:

  • Clear motivation for readable code. Code compiles down to machine instructions, and the machine doesn’t care how readable the source code is. If you never had to modify the code again, it really wouldn’t matter how it’s written. However, if a product is still in use then its code needs to be read and modified often. Thus, we need to make that code easy to work with.
  • Memorable principles. A few that stood out:
    • Complexity arises from dependencies and obscurity—too many interconnected parts, and important information that isn’t obvious.
    • Shorter code isn’t always better. Group related information together; avoid leaking internal details across features.
    • Prefer independent modules. Independence reduces cognitive load and change amplification. When modules depend heavily on each other, even small changes become risky and time-consuming.
    • General-purpose modules tend to be deeper. A reusable error-handling mechanism, for example, is almost always better than one-off solutions scattered throughout the codebase.
    • Comments are valuable when they explain why the code is written a certain way. The book pushes back on Clean Code‘s recommendation to avoid comments, and I agree with Ousterhout: numerous tiny functions can make code harder to follow than a well-placed explanation.

Where the book falls short:

  • It’s intentionally abstract and light on practical examples. This is fine for conveying philosophy, but it makes it harder to map these ideas onto real-world, large-scale systems with hundreds of developers and millions of users.
  • It doesn’t address commercial constraints. In real software development, you can’t refactor freely without risking instability for other teams. Modern engineering practices such as feature flags and staged rollouts are crucial for safe refactoring, yet the book barely touches on them.
  • Disagreement on comments vs. change descriptions. The author argues that comments should capture design intent because developers rarely read change descriptions. In practice, teams often do rely on structured change logs linked to tickets for context. In many organizations, these descriptions are the authoritative record of “why” decisions were made.
  • Minimal discussion of testing. Testing is essential for safe changes, especially when working with abstractions that may behave differently on real devices. The book would be stronger if it addressed automated testing, integration testing, and device validation as tools for managing complexity.
  • Almost no mention of design patterns. Familiar architectural patterns (MVC, MVVM, etc.) reduce cognitive load by giving developers a shared mental model. They are an important complement to the book’s principles but are barely mentioned.

Overall it’s a great book but not the complete picture of software design. It’s an excellent two-day read for engineers of all levels and a concise introduction to thinking deeply about complexity, abstraction, and maintainability. Just be aware that it won’t teach you everything you need in a modern engineering environment: version control strategies, testing practices, large-scale architecture, team coordination, or design patterns.

What it will teach you is how to write code that is readable, maintainable, and carefully designed – skills that are necessary for long-term software quality.

Summary of principles and red flags

These were taken directly from the book’s appendix.

Summary of principles

  1. Complexity is incremental – you need to care about the small stuff.
  2. Working code isn’t enough. You must be able to easily read and modify that code later.
  3. Make continual small investments to improve the system design.
  4. Modules should be deep
  5. Interfaces should make the most common usage as simple as possible.
  6. Simpler interface > simple implementation
  7. General-purpose modules are deeper
  8. Separate general-purpose and special-purpose code
  9. Different layers should have different abstractions
  10. Pull complexity downwards
  11. Define errors out of existence
  12. Design it twice
  13. Comments should describe what’s not obvious from the code
  14. Software should be designed for ease of reading, not writing
  15. Increments of software development should be abstractions not features.
  16. Separate what doesn’t matter from what does, and emphasize what matters.

Red flags

  • Shallow module
  • Information leakage
  • Temporal decomposition – code structure based on order in which operations are done instead of information hiding
  • Overexposure – don’t expose surfaces that aren’t widely used
  • Pass-through method
  • Repetition
  • Special-General Mixture
  • Conjoined methods
  • Comment repeats code
  • Implementation documentation contains interface
  • Vague name
  • Hard to pick name
  • Hard to describe
  • Nonobvious code

My notes while reading

I typed out what stood out to me while I was reading. These notes are incomplete, but someone may find them useful. I did feed my notes through an LLM to clean them up a bit.

Chapter 1: It’s All About Complexity

Our biggest limitation in software is our ability to understand the systems we build.
Simpler designs allow us to build larger, more powerful systems before complexity overwhelms us.

How to fight complexity:

  1. Make code simpler and more obvious. Eliminate special cases; use consistent naming and organization.
  2. Encapsulate complexity. Divide the system into independent modules so developers don’t need to understand everything at once.

Why waterfall fails: You can’t fully visualize the design of a large system upfront. Design issues appear only during implementation, leading to patches instead of structural fixes—amplifying complexity.

Book goals:

  1. Describe software complexity and why it matters.
  2. Provide techniques to minimize it.

Chapter 2: The Nature of Complexity

2.1 Defining complexity

Complexity is anything that makes a system hard to understand or modify.

  • It increases cost: small changes require large effort.
  • Complexity is experienced locally—whether the system is big or small.
  • Complexity should be weighted by how frequently a component is used; hidden complexity is almost as good as eliminated complexity.

2.2 Symptoms

  1. Change amplification: Small changes require edits in many places.
  2. Cognitive load: Developers must juggle many concepts.
    • e.g., manual memory management vs GC
    • multiple async mechanisms
  3. Unknown unknowns: It’s unclear where to make changes or what information is needed.

Good design makes the next change obvious.

2.3 Causes

  1. Dependencies: Code can’t be understood or changed in isolation.
  2. Obscurity: Important information isn’t obvious or visible (e.g., undocumented configs).

Dependencies → high cognitive load + change amplification
Obscurity → unknown unknowns + more cognitive load

2.4 Incremental nature

Complexity grows from thousands of small decisions and shortcuts.

2.5 Conclusion

Complexity = dependencies + obscurities → more cost for each new feature.


Chapter 3: Working Code Isn’t Enough

Good design requires continuous investment. Fix design issues when you encounter them; don’t just get something working.

Investment guideline: Spend 10–20% of development time improving design and reducing debt. Problems compound over time.


Chapter 4: Modules Should Be Deep

Goal: minimize inter-module dependencies by designing deep modules—simple interfaces with rich, hidden implementations.

Interface: What a developer must know to use the module (the what).
Implementation: How it does it (the how).

Key ideas

  • Abstractions omit unimportant details.
  • Deep modules: simple APIs, complex internals (e.g., garbage collector).
  • Shallow modules: interfaces too complex for the functionality provided.
  • “Classitis”: too many tiny classes can increase complexity.
  • Interfaces should make common cases simple (e.g., default file buffering).

Chapter 5: Information Hiding

Modules should encapsulate specific pieces of knowledge representing design decisions.

Why hide information?

  1. Reduces cognitive load.
  2. Makes systems easier to evolve.

Common issues

  • Information leakage: Multiple modules must understand shared details (e.g., file formats). Often indicates those modules should be combined.
  • Temporal decomposition: Designing modules around time sequence rather than knowledge. Focus on what’s needed to perform tasks, not operation order.
  • Too many classes: Over-separation increases duplication and coupling.

Example

Instead of exposing HTTP parameter maps, offer higher-level accessors.

Conclusion

Design modules around the knowledge required for tasks. Deep, knowledge-hiding modules reduce interface complexity.


Chapter 6: General-Purpose Modules Are Deeper

“Over-specialization may be the single greatest cause of complexity.”

General-purpose modules:

  • Have simpler, more powerful interfaces.
  • Provide better information hiding.
  • Require less code duplication.

Examples

Text editor API evolved from special-purpose methods (backspace, delete(cursor)) to general-purpose ones (insert(position, text), delete(start, end)).

Guidelines

  • Ask: What is the simplest interface that meets all current needs?
  • Push specialization outward:
    • Up into UI layers
    • Down into device/driver layers
  • Avoid special cases; aim for designs where edge cases naturally fall out.

Chapter 7: Different Layers, Different Abstractions

Higher layers should operate at higher levels of abstraction. Avoid pass-through methods that do nothing but call another method.

Pass-through variables are also problematic. Alternatives:

  • Encapsulate data in a single object
  • Use context objects
  • Avoid global variables when possible

Conclusion: Only add new structures (interfaces, classes, parameters) when they reduce complexity.


Chapter 8: Pull Complexity Downward

Prefer simple interfaces, even if implementations become more complex.
E.g., handle configuration internally rather than exposing many toggles to callers.


Chapter 9: Better Together or Apart?

Splitting modules increases orchestration overhead. Combine code when:

  • It shares important information
  • It simplifies interfaces
  • It reduces duplication

General-purpose mechanisms should live separately from special-purpose ones.

If two methods can’t be understood independently, they probably belong together.


Chapter 10: Define Errors Out of Existence

Error handling contributes heavily to complexity. Minimize exposed error paths.

Techniques:

  • Define errors out of existence: e.g., Unix delete marks files for deletion instead of failing.
  • Exception masking: Lower layers handle errors (e.g., TCP retries).
  • Exception aggregation: Group error handling into a few places.
  • Just crash: If the system is in a corrupted or unrecoverable state.

Don’t swallow errors blindly—be deliberate.


Chapter 11: Design It Twice

Create multiple designs and evaluate before implementing. Don’t go with the first idea by default.


Chapter 12: Comments Are Good

Comments reduce cognitive load and preserve intention. Not all information can be expressed cleanly in code.


Chapter 13: What Comments Should Say

Good comments explain what’s not obvious:

  • Preconditions or usage constraints
  • Reasons behind design decisions
  • High-level intent

Comments shouldn’t repeat code.
Implementation comments explain what the code is doing.

Cross-module decisions should be documented centrally.


Chapter 14: Choosing Names

Names must be precise, consistent, and free of unnecessary words.


Chapter 15: Write Comments First

Use comments as part of the design process—sketching intent before writing code.


Chapter 16: Modifying Existing Code

Each change should leave the system in the shape it would have had if designed that way from the start.
Negotiate extra time to make structural improvements as needed.


Chapter 17: Consistency

Consistency across names, styles, interfaces, and patterns reduces cognitive load.


Chapter 18: Code Should Be Obvious

Obvious code has:

  • Consistent patterns
  • Explicit important information
  • Clear, domain-specific data structures (not generic Pair, etc.)

Readers should never have to hunt for crucial details.


Chapter 19: Software Trends

Inheritance:

  • Interface inheritance = good abstraction.
  • Implementation inheritance = leaky, brittle; prefer composition.

Agile: Risk of focusing too much on short-term features instead of abstractions.

TDD: May bias developers toward feature behavior rather than design clarity.

Getters/setters: Often unnecessary boilerplate; can leak information as much as public fields.


Chapter 20: Designing for Performance

Benchmark instead of guessing. Often simpler code is faster.
Measure before and after changes; optimize only real bottlenecks.

Clean, simple designs are typically “fast enough.” Optimize only when needed.


Chapter 21: Decide What Matters

A design “matters” when it centralizes key information needed to understand the system.

Ways to emphasize what matters:

  • Prominence: Put it where it’s most visible.
  • Repetition: Reinforce key ideas.
  • Centrality: Let important concepts shape the architecture.

Avoid making everything important (clutter) or failing to highlight truly important things.

Life analogy: Identify what matters most and direct energy toward it; avoid wasting attention on low-value pursuits.

By juanito

Leave a Reply

Your email address will not be published. Required fields are marked *