Thoughts on A Philosophy of Software Design

I recently finished reading “A Philosophy of Software Design” by John Ousterhout. It’s a brief but dense book full of the author’s opinions on designing software well.

Right from the beginning, John calls out that complexity is the enemy of well-designed software. Most of the book is spent discussing either why complexity is bad or ways to avoid it when writing code.

I really enjoyed this perspective and it’s already affecting how I’m writing code, so I thought I’d share a bit of what I found most impactful.

What is Complexity in Software?

For the purposes of the book and this discussion, complexity is something in the code that makes it harder to understand, maintain, or modify. To some extent, complexity is unavoidable.

For this reason, every decision around how our software is structured and written becomes a tradeoff.

Firstly complexity increases the cognitive load of working in a codebase. More complex apps require a developer to have (or find) more information to accomplish a task. Even further, complexity causes simple changes to be amplified into more complex changes. Anyone who has ever worked with microservices knows exactly how coupling can amplify a simple change into one that requires half a dozen pull requests.

Complexity makes it harder to make changes and introduces risk into those changes.

Tactical vs Strategic Programming

Obviously, complexity cannot always be avoided.

The author of this book introduces the concept of “tactical programming”, a style of writing code that focuses on finishing a task as fast as possible. Oftentimes this approach is taken by newer developers (myself included) or those trying to optimize for velocity in an agile approach (guilty as charged).

There’s a benefit to tactical programming. Businesses need to make money, and developers get paid to make that happen. Like I said, everything is a tradeoff. The author argues that developers should strive to do what he calls “strategic programming”, where the primary goal is to produce a good software design that also happens to work*.

Strategic programming can be justified in software projects as an upfront investment in the future development velocity of a project.

Tactical programming naturally results in more complex codebases that grow in complexity with exponential speed as simple changes continue to amplify into complex ones. Left unchecked, this results in a mountain of technical debt, with teams paralyzed by necessary refactors before any features can be built.

Conversely, the author argues strategic programming has the opposite effect on a project. Taking longer to implement things because of an investment in good design pays dividends, as all future changes are easier to make.

John has a strong opinion here and seems to insist that tactical programming is always bad. I personally think there are times where it makes sense to just get something done, though after reading this book the likelihood I would do that is greatly reduced.

Deep Software Modules

Modular design puts emphasis on writing code so that only a fraction of a systems complexity must be dealt with at a given time, reducing cognitive load, and therefore complexity.

Breaking things into modules, classes, or even microservices is an incredibly common way to write software. What is unfortunately not as common, is breaking things into deep modules.

Modules that are deep have minimized dependencies on other modules. Modules that are deep abstract functionality away from other modules. Modules that are deep provide functionality while requiring little understanding or information from another module.

The author argues that we should design software with modules that have simple interfaces, but powerful functionality.

Specifically, the author pushes the idea that we should pull complexity downward into a module. When designing a method, it’s more important that the method have a simple interface than a simple implementation. This scales to classes or even services as a whole. Every piece of complexity in the interface of a module introduces coupling to anything that uses it.

How Naming in Software Affects Complexity

One really easy way to reduce complexity in code and improve readability is to use meaningful names. I’ve been guilty of using abstract variable names like data only to later annotate the variable with a comment like // the location the user entered to describe the usage.

It’s far more useful to just name the variable what it actually represents, as it makes the code far easier to understand. Easier to read means less complexity!


This book contains a lot more detail (and a few more topics) that I didn’t get into here. These are the things that stood out the most to me as immediately useful in my day-to-day of writing code and squashing bugs. Still, you should go read it, no way you regret it 🙂