Review: A Philosophy of Software Design by John Ousterhout

The book presents the author’s view on software design principles that he developed through multiple software projects, as well as through teaching software engineering at Stanford University. His teaching approach focused on giving feedback and asking students to refactor their code so that their software would not only compile and work, but also be well designed.

The main theme of the book is that software design is about fighting complexity. Apparently, this matches well the observations described in the Systems Thinking and Systems Engineering courses regarding many types of systems, not only software-intensive systems.

The author identifies two main ways to reduce complexity:

  1. write clean, simple code while trying to avoid special cases;
  2. modularise the code so that complexity is hidden behind simple interfaces.

It is noted that software complexity is often more apparent to readers than to the writer. Therefore, new code should be reviewed.

It has become the norm in industry that software development is incremental. The waterfall approach rarely works for software, as software is intrinsically more complex and flexible than physical systems. The initial design will often have many problems that become apparent only once development is well underway.

What typically happens is that software systems evolve over time, and issues are patched or new features are implemented without redesign. This leads to an explosion of complexity.

Most programmers work with a tactical programming mindset — completing the immediate task at hand as fast as possible, without considering how that affects the overall system design. Unfortunately, complexity grows incrementally. It’s easy to convince yourself that a little bit of extra complexity is acceptable, but this accumulates over time as others do the same.

Once you start down the tactical path, it becomes difficult to change direction. The messier the system becomes, the harder it is to improve it, so it becomes easier to justify yet another “quick fix”.

Therefore, the author recommends a zero-tolerance approach toward quick-and-dirty fixes. It is not acceptable to introduce unnecessary complexity just to finish the current task faster. Most code is written by extending an existing codebase, so one of the most important responsibilities is to facilitate future extensions.

A tactical tornado — a developer who produces large amounts of tactical code that others cannot keep up with or maintain.

Incremental strategic development means continuous redesign. For that, resources are needed. Strategic development includes:

  1. thinking ahead when writing new code;
  2. continuously improving system design bit by bit.

The author suggests investing 10–20% of development time into strategic improvements. This investment may feel large, but it pays off quickly. A well-designed system saves far more time in the future than what was initially invested in improving its design.


Selected Design Principles

The following list includes only some of the design principles proposed by the author. The book provides extensive explanations accompanied by code examples for each recommendation.

Deep modules are preferable to shallow modules.
A deep module may be internally complex, but its interface is simple. In other words, the interface hides a large amount of internal complexity.

Information leakage should be avoided.
Information leakage occurs when two modules rely on shared knowledge (not necessarily shared data, but possibly shared assumptions). For example, two classes that rely on a particular file format leak that information into each other without this dependency being visible in the formal interface. Software engineers should develop a strong sensitivity to information leakage. Modules should be as independent as possible. Code that relies on shared knowledge should be placed close together (preferably in a single module).

Separate specialised code from general-purpose code.
For example, generic algorithms operating on data structures should not be mixed with application-specific domain logic.

Pull complexity downward.
Do not offload complexity onto the users of your module. Ask yourself whether the user really knows better what to do in a particular situation. Avoid excessive configuration whenever possible. If configuration is necessary, provide reasonable defaults so that most users do not need to worry about it.

No one gets it right on the first try.
Even experienced software engineers need multiple iterations to achieve good design.

Documentation matters.
Code documentation (especially API documentation) is essential. Even good code is not truly self-documenting, because design decisions, assumptions, side effects, and context are rarely obvious from reading the code alone. One would need to read (and fully understand!) the entire codebase to infer this information, which is neither feasible nor desirable. This also contradicts the principle of hiding complexity: APIs are supposed to hide complexity, so users should not need to read the implementation. In-code comments typically operate either below or above the abstraction level of the code. They either provide low-level details or higher-level intuition. For example, descriptions of invariants are excellent low-level comments.


I consider this book a must-read for any software engineer. I only wish more teams consistently followed the design principles it promotes.

2 лайка

Really? In our time, with the booming rise of autonomous coding agents?

Are you saying people won’t design any software?

1 лайк

By hand? Only by fun.

1 лайк

Чего это у нас тут за разговорный инглиш клуб нарисовался?)

1 лайк

How will you handle software architecture design without understanding basic design principles? Or how will you reorganize it if something goes wrong? By just trusting AI to cover it for you? Are there any risks?