If my experience in software engineering has taught me one thing, it’s that simplicity is king.
As a developer for 20+ years — from senior software engineer at Stack Overflow to my current role as Chief Technology Officer at Intelligent Hack — I’ve been hands-on, working on the ground at Stack Overflow, the world’s largest developer community, and I’ve seen a lot of things change over the years.
But whatever the project, client, or brief, I always apply one core principle to every piece of code: Keep it simple.
This is a recap of a recent Online Technical Workshop we hosted. Watch the full presentation below or skip down to the recap!
To me, simplicity is fundamental. As Gall’s Law states (in John Gall’s book, Systemantics: How Systems Really Work and How They Fail), “A complex system that works is invariably found to have evolved from a simple system that worked. A complex system designed from scratch never works and cannot be patched up to make it work. You have to start over with a working simple system.”
Simple systems work because they’re easy to understand, modify, and repair. They’re more accessible to a wider range of users, and more often than not they run faster too. There are no hidden surprises and no unexpected results.
Complex systems, by comparison, are magical. Nobody knows exactly what’s going on. How are processes executed, and why does X happen when you do Y? It’s difficult to train somebody to use a complex system, and those gaps in knowledge become increasingly problematic as new users come and go.
This is not to suggest that simple systems can’t do complex things. Some problems — build and deployment, for example — are inherently complex. However we should all be wary of introducing accidental complexity into a system. Organizing and structuring code, employing architectures, and choosing flexibility or rigidity, can all create accidental complexity. Thus when we strive for simplicity, it should be systemwide, from deployment to running, monitoring, and beyond.
Six complexity pitfalls
So where do we find complexity? In my experience, there are six key areas where accidental complexity is often introduced into a system. By avoiding this complexity, you’re free to focus on what really matters — the inherent complexity of the problems your system is trying to solve.
Decoupling code is great for adding flexibility — when you need it. Remember every time you decouple, you add complexity. This makes it more difficult to add large-scale refactoring. Highly decoupled architecture locks engineers into designs that obscure the big picture. Consider it a tradeoff between facilitating a particular application and the simplicity of the overall system, and ask yourself if decoupling is really necessary every time.
2. Design patterns
The Gang of Four says design patterns should “favour composition over inheritance.” It’s a rule of thumb that often gets overlooked by developers. Design patterns add complexity by introducing structures and interactions that can lead to over-abstractions. Instead of implementing design patterns when you build your system, you should discover them as they emerge from the code. This ensures you create and use the patterns that are right for your system, rather than the ones you think will be right.
Many developers are heavily influenced by Uncle Bob’s SOLID principles of object-oriented design. However it’s important to consider the environment in which he was working. Game and web dev are worlds away from telecommunications, governed by different constraints and principles. SOLID isn’t bad, but it usually isn’t relevant to modern programming, and can introduce unnecessary complexity in a number of ways.
4. Premature abstraction
Before you follow the DRY principle, you need to be able to identify abstractions in your code. That requires examples. It’s simpler to wait until you have 3-4 copies of an element before extracting the commonalities to create code that doesn’t repeat itself. Equally, don’t generalize your code in anticipation of future needs. Instead only make adjustments for reasonable expectations, and work with what you see in front of you.
5. Useless code
They can happen to the best of us. Little bits and pieces of code creep in that don’t belong. Classes so small they shouldn’t exist, single-implementation interfaces, and design patterns that don’t add value, are all common culprits. That’s why it’s important to review your code to find and eliminate anything that’s making your system more complex.
6. Over reliance on libraries
How much work do libraries do in your system? Too much and what happens to your system if the library is dropped? Too little and you’re introducing significant amounts of code you’re not even using. Libraries create complexity by removing your control over your own system. They can produce unnecessary compatibility errors and slow or even crash your system. If you can replace them with a few simple lines of code, do so.
Final thoughts: Always remember Gall’s Law
Simplicity and complexity are always judgment calls. There is no standard or benchmark, and each type of development has its own constraints and needs. You must remember to take a high-level overview of your system and its needs, be vigilant when eliminating complexity, and look for the patterns that emerge from the code. Always remember Gall’s Law and strive for simplicity at the outset in order to create complex systems that really work.