A Philosophy of Craft: Abstractions and Entropy in Software Engineering
Where the job of the programmer ends, the job of the software engineer begins.
I don’t claim this to be a definitive guide or an exhaustive list of pointers. In fact, I acknowledge that my thoughts may not resonate with everyone. As I gain more experience as a software engineer, I’ll likely revisit and refine these ideas.
I hope I’m not underselling it when I say software engineers carry a huge responsibility toward humanity. As our interaction with the world increasingly happens through software, this responsibility grows even larger. Just like civil engineers are entrusted with building safe and durable bridges, software engineers have a similar obligation to create reliable and robust systems.
You don’t want your software to glitch when buying or selling stock options or when trying to book an Uber with only 1% battery left on your phone. Poorly written code doesn’t just lead to confusion or frustration for end users — it can create a domino effect. Even if your code is technically correct, if it’s poorly engineered, it could waste hours of development time and cause unnecessary stress for other engineers. Those who work on top of your code might struggle to iterate, integrate new features, or connect additional modules, resulting in time lost that could have been better spent on productive tasks.
Murphy’s law states that anything that can go wrong will go wrong — and usually at the worst possible time.
Now that I’ve established the importance of software engineering, let me share some of my key learnings, which are the focus of this post.
Software engineering, first of all, is a painful job. Programming, on the other hand, may or may not be a painful job. A good software engineer wants to ensure the software doesn’t burst into flames now or in the future. However, they also know for a fact that things will go wrong. Remember Murphy’s law? Anything that can go wrong will go wrong — and usually at the worst possible time. So, they plan for contingencies, mitigation measures, and fail safes. When I write about software engineering, I write with an average engineer in mind — someone working in a team of reasonable size, trying to do better each day.
You want to make it difficult for people working with your code to make mistakes. I think much of the wisdom authors include in their books comes from suffering — either their own mistakes or those of others. As a result, much of their advice is about making your engineering robust and eliminating obvious red flags within real-world time constraints. For example, if your manager expects a big deliverable within a fortnight, you can’t afford to be an idealist.
Using abstractions helps reduce potential disasters and time leaks. Making assumptions is a problem. Don’t let people working with your code make assumptions. Instead, design your code to make incorrect assumptions difficult or impossible. Hide implementation details as much as possible and expose only the outputs and interfaces that others need to work with your code. Your API should be solid and self-explanatory, allowing all clients interacting with it to understand what to expect.
Even in your own work, when tackling a project of considerable size, it helps to think in terms of layers of abstraction. Start with the lowest level, implement it, and then move up a layer. At the upper layer, you shouldn’t need to worry about the lower layer’s implementation — it should simply function as an abstraction or interface. I find this approach highly liberating because it frees up mental bandwidth, allowing me to focus on one problem at a time.
A significant realization for me came during some intense, time-bound projects at FIEA. I understood that you might not always produce the ideal architecture. If you insist on finding the perfect design before starting, you might never begin. A core philosophy behind design patterns is creating code that isolates parts, so each has a limited sphere of influence. This means that when issues arise, you don’t have to search through 100 different files. It also ensures that your mental model of the code — structured in layers of abstraction — enables independent analysis of each block without delving into the logic of others.
Avoid multiple endpoints or channels for modifying a piece of code or data. When a variable or object is updated in arbitrary ways across multiple files, maintaining the code becomes very challenging. Instead, define a single function or a clear set of methods for modifying the state or data. This way, you know where to look when making changes, and you can conform to a consistent API. Moreover, when debugging, you won’t waste time isolating issues caused by numerous arbitrary modifications.
It’s important to alternate between zones of rapid work — writing fast, dirty code without worrying about patterns or the “right” decisions — and zones of thoughtful refactoring. Once you have a working prototype, think about how to reorganize the functionality so it fits a clear mental model, mitigates problems, and aligns with good engineering practices.
Dan Bekins visited FIEA to give a talk on software engineering principles, during which he introduced the concept of entropy in programming — an analogy I found profoundly enlightening. I haven’t come across this idea framed quite this way anywhere else. Let me share what I understood from his talk, but first, let’s establish some basic facts from physics before diving into the concept.
Entropy: Particles in nature tend to spread chaotically, and energy disperses over time.
Usability of energy: Energy is more usable when concentrated and less usable when spread out.
Programming behaves similarly. Every change you make increases the system’s entropy. The more moving parts you add, the greater the system’s entropy. Sometimes the relationship between your work and the resulting entropy is linear; other times, it’s not. Even simple modifications can significantly increase the system’s entropy, sometimes exponentially. Considering the law of compounding, this can make the situation even more unmanageable. It falls to the astute software engineer to expend additional effort to reduce the system’s entropy while working within constraints and without compromising the original purpose behind the change. Where the job of the programmer ends, the job of the software engineer begins. I believe this philosophy of entropy succinctly describes the role and responsibility of a software engineer.
That’s all I had to share. I hope some of the points above offered new perspectives or provoked you to reexamine some of your own ideas.