I finally sat down with John Ousterhout’s A Philosophy of Software Design after hearing engineers rave about it for years. And to be honest, the first half felt… obvious. At least on the surface.
But as I kept reading and especially as I revisited the ideas with my own experience in mind, something clicked. The book isn’t impressive because it says revolutionary things; it’s impressive because it says fundamental things with sharp clarity. It gives names to problems we deal with daily, and it offers clean mental models for recognizing and fighting complexity.
This post distills the ideas that stuck with me the most, the concepts I know I want to carry into my work from this point onward.
1. Complexity Isn’t a Monster, it’s a Slow Leak
Ousterhout’s definition of complexity is incredibly practical:
Complexity is anything that makes code hard to understand or modify.
There are no medals for clever abstractions, no extra points for patterns used, and no trophies for “clean code.” The question is brutally simple:
Is this code easy to work with or not?
What really hit me was his focus on how complexity accumulates: not in huge architectural decisions, but in tiny micro-decisions repeated hundreds of times:
- That extra condition we didn’t refactor
- That helper method that just forwards arguments
- That duplication we promise to clean later
- That general-purpose class that handles “just one more case”
Individually harmless, collectively destructive.
2. Deep Modules: The Most Valuable Idea in the Book
This is the idea that completely reframed how I judge my own code.
A deep module has:
- a simple interface
- and rich internal functionality
A shallow module has:
- a large or awkward interface
- but very little useful functionality inside
The more I thought about it, the more I realized how often we accidentally create shallow modules, things like:
def set_nil_for(attr)
@data[attr] = nil
end
or a React component that is nothing but a prop-passing wrapper.
A shallow module adds another name, another file, another place to look but hides nothing. The interface complexity remains the same.
A deep module, on the other hand, absorbs complexity so callers don’t have to think.
This has become a new personal red flag: If the interface isn’t dramatically simpler than the implementation, something’s wrong.
3. Pass-Through Methods Are a Smell — Not a Style
We often celebrate “clean code” that has tiny methods. But tiny methods tend to create pass-through behavior:
def user_name
profile.user.name
end
Receivers simply forward calls without adding value. Ousterhout calls these methods out as aggressively anti-design.
Why?
Because every extra layer:
- deepens the call stack
- adds another place to navigate
- increases surface area
- reduces clarity rather than improving it
I realized I had internalized the “small methods” rule from early-career programming. The book forced me to reexamine it.
Small is not the goal. Leverage is.
4. Pull Complexity Downwards
This one clicked immediately because it mirrors lessons I learned the hard way in production systems.
The rule:
If someone has to deal with complexity, let it be the module, not the caller.
Callers should see a clean, obvious API.
Example: Instead of forcing the caller to handle:
if user.email.present? && !user.unsubscribed?
NewsletterJob.perform_async(user.id)
end
Push this down:
NewsletterSender.enqueue(user)
Inside it:
- validate
- check business rules
- handle edge cases
The calling code stays simple. The system becomes easier to work with.
5. Don’t Mix General and Special Cases in One Place
This is the silent killer of many codebases.
When a module tries to be both:
- general purpose
- and application-specific
…it becomes neither.
Example:
class Notifier
def notify(user)
# general behavior...
send_discount_message(user) if user.premium?
end
end
This pollutes the abstraction with business logic. The result is a module that’s supposed to be reusable but actually isn’t. The special case leaks into what should have been a clean general interface.
This red flag (general–special mixture) is now firmly on my mental dashboard. If a conditional refers to a business concept, it doesn’t belong in a general-purpose abstraction.
6. Define Errors Out of Existence
This is one of Ousterhout’s most elegant ideas.
Instead of designing APIs that throw errors expecting callers to do the right thing…
…design APIs that cannot produce those errors.
Example:
Instead of:
File.delete(path) # fails if path doesn't exist
Force callers to check existence, or throw exceptions, or handle edge cases.
Better:
# Better: The API ensures the file is gone, no matter what.
# No need to check existence first.
FileUtils.rm_f(path) # Deletes if exists, does nothing if missing. No error.
The API absorbs the complexity so callers don’t have to.
The mental overhead evaporates.
7. Comments Aren’t a Burden — They’re a Design Tool
This one surprised me.
His argument is not “write more comments.” It’s:
Write comments that explain things the code cannot express — especially why something is done a particular way.
But his bigger advice is even bolder:
Write the comments before you write the code.
At first that felt strange, but I tried it. Writing the comment forced me to clarify:
- the purpose
- the assumptions
- the required behavior
- the design choices
Before writing a single line of code, I had already improved the design.
It’s like writing a miniature spec — but right on top of the method or class that implements it.
I found this surprisingly effective.
What I’m Carrying Forward
Three red flags now sit front-and-center in my mind whenever I design or refactor:
-
Shallow modules If a module doesn’t hide complexity, it’s not helping.
-
Pass-through methods If it only forwards calls, delete it or find the real abstraction.
-
Mixing general and special cases Separate business logic from reusable logic. Always.
These alone will significantly reduce complexity in the systems I build going forward.
Conclusion
I didn’t finish the book thinking “wow, I learned new concepts.” I finished it thinking:
These principles sharpen what I already knew, and make me apply them with intention.
Software design isn’t about patterns, or cleverness, or even elegance.
It’s about reducing cognitive load. It’s about making code obvious. It’s about designing so future-you and future-teammates don’t suffer.
These are the principles I now use to evaluate every abstraction I touch, especially in legacy systems where complexity has already compounded.
And in that sense, A Philosophy of Software Design is one of the clearest guides I’ve read.