I recently read “100 Go Mistakes and How to Avoid Them” and came across a phrase that made me ponder for a while. The book discusses the decoupled nature of the Go language, where types aren’t tied to a specific interface like in most statically-typed languages. Instead, they are implicit as long as the type meets the interface’s requirements. This decoupling allows you to define interfaces in a generic, loose sense, but they should only be applied after identifying practical use cases. Since types don’t necessarily require an interface, the book recommends not creating interfaces preemptively for the user, allowing them to define these as needed for specific use cases. This led to the phrase “abstractions are discovered, not created.”
When I encountered that phrase, it resonated with me beyond the context of Go’s loose interface typing. I thought of it in terms of code abstraction when architecting larger features. I’ve worked on projects that were heavily abstracted, leading to code that is difficult to follow due to the numerous layers one must understand to grasp what is happening. Some of you may have tried to trace the logical route of an API call, diving into function calls that lead to more functions, and so on. Before you know it, you’re deeply entrenched in function calls, struggling to visualize all the pieces and how they fit together. You find yourself revisiting parts to remember the logical flow of the API call. This scenario is a common result of overabstraction.
We all understand the desire for DRY (Don’t Repeat Yourself) code. Abstractions allow us to take repeated code and centralize it for easier maintainability. However, every engineering decision, no matter how minor, has its pros and cons, and abstraction is no exception. It’s crucial to abstract for the sake of DRY code and maintainability, but it’s also important that your code is written for humans, not just machines. Machines don’t care about clean, dirty, DRY, SOLID, or any other programming principles — they run the code as is. What really matters is the human reading the code, whether it’s you six months from now, or a new developer trying to get up to speed.
Consider this example of potential overabstraction:
def isEven(number):
return number % 2 == 0
Where do we draw the line between this definition of overabstraction and DRY? This is a long-standing opinion among many programmers that has no definitive answer. However, we can follow principles rather than rigid rules. Principles allow us to make common decisions without adhering to strict rules. A common simple abstraction is formatting a datetime. You may use it once or even a few times, but quickly realize this should be a common function so you’re not painstakingly changing the datetime format if needed later on. This is an abstraction discovered while you were writing code.
As you develop a feature, you may discover abstractions that can be helpful. Weigh the pros and cons of these abstractions: Does the abstraction make the code’s intention clearer to readers? Does the abstraction outweigh the added mental load of another function context while reading through the feature logic? Does it make the code easier to maintain? Could you quickly make a grep change in a few places, or would finding that utility function be quicker? Finding the right balance in code abstraction is more of an art than a science. It requires a deep understanding of both the language you are using and the needs of your project. While the principles of DRY and SOLID provide a framework, they should serve as guides rather than strict laws. Every line of code you write is a decision point, and it’s these decisions that define the maintainability and clarity of your application over time.
As you continue to code, remember that abstractions should simplify complexity, not mask it. They should make your code more intuitive and maintainable for anyone who might read it in the future—including yourself. Always ask whether an abstraction adds clarity to the overall design or just adds another layer of complexity that could confuse others. By approaching abstraction with a critical mind and a focus on practical application, you’ll find that the best abstractions are indeed discovered in the process of solving real problems, not by enforcing them prematurely. Keep this in mind, and watch your codebase not only grow in functionality but also improve in quality and robustness.