You’re staring at a screen filled with three hundred lines of nested if-statements and a variable named temp_list_2. It’s 2 AM. You wrote this code three months ago, and honestly, you have no idea what it does. This is the "tax" of complexity. Most people think great software is about knowing every framework or typing 100 words per minute, but it’s actually about how you think. It's about your philosophy of software design.
Complexity isn’t some accident that happens to bad developers; it’s a natural force, like gravity, that pulls at every project. If you aren't actively fighting it, you're losing. John Ousterhout, a professor at Stanford and the author of the actual book A Philosophy of Software Design, argues that the greatest challenge in computer science is managing complexity. He’s right. But the way we’ve been taught to do it is often totally backwards.
The Complexity Rabbit Hole
What is complexity anyway? It’s basically anything that makes a system hard to understand or modify. It shows up in three ways: change amplification (one small change requires touching twenty files), cognitive load (you have to keep too much in your head to finish a task), and "unknown unknowns" (you change something and accidentally break a feature you didn't even know existed).
Most developers try to solve this by making everything "small." They create tiny classes and three-line functions. They think that if a file is short, it's simple. This is a trap. When you break a system into too many tiny pieces, you haven't actually reduced complexity; you've just moved it. Now, instead of having a complex function, you have a complex interaction between fifty different tiny functions. It’s like trying to read a book where every sentence is printed on a different page scattered across the room. Good luck with that.
Deep vs. Shallow Modules
The real secret to a solid philosophy of software design is the concept of "Deep Modules." Think of a module like a rectangle. The top edge is the interface—what the world sees. The area of the rectangle is the implementation—the actual work it does. A "shallow" module has a huge interface for very little functionality. It’s a lot of noise for not much signal.
A "deep" module, on the other hand, provides a massive amount of power through a very simple, tiny interface.
Take the Unix I/O model. It’s incredibly deep. You have five basic calls: open, read, write, lseek, and close. That’s it. But behind those five simple words lies a universe of complexity handling disk drivers, file systems, caching, and hardware interrupts. You don't need to know any of that to save a text file. That is design perfection.
Contrast that with some modern Java or C# "Manager" classes that require you to pass in ten different configuration objects just to print "Hello World." That’s shallow. It forces the complexity onto the user of the class rather than hiding it inside the implementation.
👉 See also: How to Cancel Spectrum Internet Service Without Losing Your Mind
The "Tactical" vs. "Strategic" Mindset
Most companies hire tactical programmers. These are the "get it done" folks. They’re fast. They close Jira tickets like they’re winning a prize. But tactical programming is a debt trap. When you’re tactical, you’re just looking for the shortest path to a working feature. You hack in a quick fix, add another flag to a function, and move on.
Over time, these "quick fixes" accumulate. The code becomes a minefield. Eventually, the tactical speed drops to zero because every new feature requires a massive rewrite of the old, brittle stuff.
Strategic programming is different. It’s the belief that the primary goal isn't just to make the code work, but to produce a great design. It takes about 10-20% longer in the short term. You spend time thinking about how a module should look before you type a single line. You refactor as you go. You realize that "working code" is not the same thing as "finished code."
Working Code is the Bare Minimum
If your code works but it’s hard to read, you’ve failed. Sorry. That’s the reality of professional software. Code is read way more often than it’s written. If you save an hour today by being messy, you’re probably costing someone else (or your future self) ten hours next month.
Information Hiding (And What People Get Wrong)
You’ve probably heard of "encapsulation." Most people think that just means making variables private and creating getters and setters. That’s not information hiding. In fact, if you have a getter and setter for every private variable, you haven't hidden anything at all. You’ve just added more boilerplate.
True information hiding means the module's users should not know about the internal mechanisms, period.
Let’s say you’re building a system to manage a list of users. A "shallow" approach would expose that you’re using an ArrayList. Now, every other part of the code knows it’s an ArrayList. If you ever want to change it to a Hash Map for performance, you have to rewrite the whole system. A "deep" approach would just give you a findUser() method. The fact that it’s an ArrayList, a database query, or a text file is a secret.
🔗 Read more: How Much VR Headset Costs Really Are (and the Traps to Avoid)
The Disaster of "Leaky Abstractions"
When an abstraction "leaks," it means the underlying complexity is poking through the interface. If you're using a database library but you still have to manually manage connection strings and retry logic in your business logic, the library is leaking. A good philosophy of software design demands that we plug those leaks. If you find yourself writing the same "glue code" every time you call a specific module, that glue code belongs inside the module.
Why Comments Aren't a Sign of Failure
There’s a weird trend in the industry saying "good code should be self-documenting." It sounds great. It’s also mostly nonsense.
Yes, your variable names should be clear. Yes, your functions should be well-structured. But code can only tell you what it is doing. It can almost never tell you why it is doing it or what the developer was thinking.
- Code:
if (timeout < 100) { ... } - Self-documenting code:
if (timeout < MIN_THRESHOLD) { ... } - The "Why" (The Comment):
// We use 100ms because the hardware controller resets if the signal stays low longer than that.
The code can’t tell you about the hardware controller. Comments should capture things that aren't obvious from the code. If you find it hard to write a comment for a function, it’s usually because the function’s design is confusing. The act of commenting is actually a design tool. If you can’t describe what a function does in one or two simple sentences, your function is probably too complex.
Define Errors Out of Existence
Error handling is where most software goes to die. It’s often the most complex part of a system because there are so many edge cases.
A sophisticated philosophy of software design suggests that instead of "handling" errors, you should try to "define them out of existence."
Take the substring method in some languages. If you pass an end index that is smaller than the start index, it throws an exception. You have to handle that. But what if the method was defined to just return an empty string in that case? Or what if it automatically swapped the indices? By changing the definition of the method, you’ve removed a whole category of errors that the user has to worry about.
Obviously, you can’t do this for everything. You can't "define away" a lost internet connection. But you can certainly reduce the number of exceptions your users have to catch by making your methods more robust and predictable.
Real-World Case: The Tcl Scripting Language
John Ousterhout created Tcl. One of its early design wins was how it handled strings. In Tcl, everything is a string. This sounds crazy and inefficient, right? But from a design perspective, it was incredibly deep. It meant that every single command could talk to every other command without any translation layers. The interface was uniform. It hid the complexity of data types from the user, allowing for massive flexibility. While it had performance trade-offs, the "depth" of that design choice allowed Tcl to punch way above its weight class for years.
How to Start Improving Your Design Today
You don't need to rewrite your whole codebase to apply a better philosophy of software design. Start small.
First, look at your most recent pull request. Find a module or a class and look at its interface. Can you make it smaller? Can you take a piece of logic that the caller is currently doing and move it inside the module? That’s the easiest way to add depth.
Second, stop being tactical. Next time you have a "quick bug" to fix, don't just patch it. Spend twenty minutes looking at the surrounding code. Ask yourself: "Why was this bug possible in the first place?" If the answer is "the design is confusing," fix the design.
Third, invest in your comments. Don't just explain the if statement. Explain the business rule. Explain the weird edge case you found during testing.
Actionable Next Steps
- Audit your interfaces: Pick one class in your project. List every public method. If there are more than five, ask if some can be combined or made private.
- Identify "Shallow" code: Look for classes that do almost nothing but pass data to other classes. These are candidates for refactoring or deletion.
- Practice "Design Sense": Before writing a new feature, draw the modules on a piece of paper. If the lines between them look like a bowl of spaghetti, your code will look like that too.
- Read the Source: Go look at the source code for a successful "deep" library, like the Python
Requestslibrary or the SQLite core. Notice how they hide massive complexity behind a few simple function calls.
Design is an iterative process. You’re never going to get it perfect the first time. But if you stop obsessing over "lines of code" and start obsessing over "depth," you’ll find that your software becomes easier to build, easier to maintain, and a whole lot more fun to write. Honestly, your future self will thank you when they aren't debugging at 2 AM.