Tim Etler

The Philosophical Programmer

June 04, 2025

The Pragmatic Programmer (Andrew Hunt/David Thomas, 1999) has withstood the test of time as essential reading for developers early in their careers. Its practical advice and actionable pragmas work well when your primary job is shipping features and business logic. The patterns and advice it provides offer immediately applicable actions that developers can integrate into their development process.

Where I find it lacking is in providing a core mental framework for the art of software design. This becomes much more important as you grow beyond writing features for end users to architecting platforms for other engineers. That’s where A Philosophy of Software Design (John Ousterhout, 2018) steps up and provides the philosophy you need to tackle tough architectural problems. The book breaks down software design into fundamental first principles to construct mental frameworks, then re-derives best practices from that foundation.

The core abstraction the book identifies as the philosophical basis for writing good software is complexity. Software design is all about managing complexity, and good design minimizes complexity while maintaining expressiveness. Complexity directly leads to increased cognitive load, which impacts our ability to build on top of a system and reduces efficiency. The framework through which complexity is managed is interfaces. The ways that information flows through a software system or any other information system is an interface.

Learning from Music Theory

I like to compare it to music theory. A musician who writes a song may have intuited patterns and practices that correspond to music theory without actually understanding the study of the theory describing the emergent behaviors behind their music. This can be completely fine for a musician who occasionally writes their own music that they perform themselves, but what about a composer, who’s primary job is to do nothing but write music for other musicians? Not only do composers write music for other musicians, they must also structure their work in a way that other musicians can more easily understand. Composers can greatly improve their output and job performance by understanding music theory, and applying that study to their work. In the same way that feature developers benefit from practical advice, developers working at the architecture level can benefit greatly from studying theory and philosophy around software design.

Both music theory and software design recognize patterns in the emergent properties of their foundational medium. In music, the foundational medium is sound waves, the physical basis that all music is built on. In software, the foundational medium is information and how it flows from one part of a system to another.

The behavioral characteristics of sound waves exhibit emergent mathematical patterns in how they interact with each other. The abstraction on top of music’s physical medium is patterns, and the patterns that sound harmonious to humans are dictated by the physical characteristics of sound waves. In software, that abstraction is complexity, and there are intrinsic properties of complexity that emerge from the flow of information.

That abstraction has a human expression in how we experience it. In music theory, that’s the tension and release you feel from those patterns, and composers play with these dynamics. In software, the human expression of complexity is cognitive load. As complexity increases, so does our cognitive load and our frustration in understanding the information system.

These abstractions and expressions can be conceptualized into mental frameworks that help us organize our thinking. In music, this framework is musical form, the structural principles that guide how we arrange and organize musical elements. In software, our framework is interfaces, the systematic ways we design how information flows between different parts of a system.

  Music Software
Medium Sound Waves Information
Abstraction Patterns Complexity
Expression Tension & Release Cognitive Load
Framework Musical Form Interfaces

Deriving Best Practices from First Principles

When thinking philosophically and theoretically, you can take that kind of meta study and apply it to your practice. You can then derive the pragmatic advice you’ve been following from first principles. If everything is an interface, then that includes all forms of data flow. Function I/O is of course an interface, but so are module imports and exports, error throwing, global variables, and configuration. Even side effects can be viewed as a different form of interface providing a different way data can flow.

The advice of keeping your function interfaces and modules clean, not throwing exceptions for expected errors in program behavior, and avoiding global variables and side effects all become united under a single mental framework. That unified worldview can inform every aspect of how you design and architect software systems. Instead of memorizing pragmas, you can derive best practices for conceptual building blocks that can be applied more broadly.

While I have intuited most of the lessons that can be derived from Ousterhout’s mental framework over years of trial and error, the structured thought process and meticulous study into the behavior of complexity in software design has helped me organize and unite my thinking. The framework, nomenclature, and behavioral patterns observed by Ousterhout are invaluable, and make his book something I believe should be required reading for all software developers.

Becoming a Philosophical Programmer

Being pragmatic is a great way to get started and navigate the complex world of computer programming. I believe the best way to move beyond that to designing and architecting software and information systems is to become a philosophical programmer. When reading A Philosophy of Software Design, don’t go in looking for specific pragmas to follow. Instead, try to find the conceptual patterns behind software patterns broadly so you can apply the lessons and philosophy flexibly. Shift from focusing on simply getting the next task done to thinking abstractly about the underlying problem to find better, simpler solutions.