How to Avoid Excessive Software Abstractions

Without being discerning around abstraction, you can run the risk of over-using it in a way that adds more complexity to a system.

Written by Isaac Feldberg
Published on May. 17, 2022
Brand Studio Logo

Abstraction extends well beyond software engineering. Living in the modern world, you encounter it every day. 

Consider the humble coffee maker. As you roll out of bed in the mornings and shake off the previous night’s sleep, general wisdom holds that you’d be well-advised to brew a strong, well-balanced cup of joe. But whether you opt for the filter-machine lifestyle or prefer a bean-to-cup approach, making coffee tends to be one of the more convenient processes you can commit to in the kitchen, often requiring little more than the push of a button. 

Naturally, this is by design. Though you’ll have to know how to add water and beans and turn the machine on, what’s happening inside to produce a delicious cup of coffee is less visible — and not altogether necessary for you to know, given that engineers took care of those internal details and created a simple interface for caffeine enthusiasts to contend with. That’s abstraction in action — as is flipping a light switch or turning a car key in the ignition. 

For software engineers specializing in abstraction, the allure of delivering intuitive user experiences is clear. But as with caffeine intake and all other things, moderation is key; without being discerning around abstraction, you can run the risk of over-using it in a way that adds more complexity to a system.

“There are no silver-bullet answers in terms of when to use or not use an abstraction,” said Eric Vincent, principal engineer at Aetion. Instead, continually reevaluating a software solution is typically necessary for this healthtech company’s team member to assess whether or not a given abstraction will simplify the user’s experience or have the opposite effect.

At CLEAR, a secure ID platform, Noah Weinthal similarly considers abstraction on a case-by-case basis. As an engineering manager specializing in healthcare systems, he told Built In he relies on several “rules” to avoid excessive abstractions in his codebase, from “abstract organically, not artistically” to “be careful not to design problems to meet your solutions.” 

Below, Vincent and Weinthal told Built In about their approach to managing software abstractions and avoiding the various pitfalls that can accompany them.


 

Image of Noah Weinthal
Noah Weinthal
Engineering Manager, Healthcare Systems • CLEAR

In addition to helping travelers get through airport security using biometric data, CLEAR’s identification technology recently expanded into other industries with a Health Pass application that can confirm a person’s identity and connect it to relevant medical information — including Covid-19 vaccination status.

 

Tell me about a necessary and an unnecessary abstraction you’ve encountered within your codebase.

Working on Health Pass has required constant re-evaluation of our assumptions and abstractions, as regulatory and policy landscapes continued to evolve and shift over the course of the pandemic. There’s significant variance in how Covid-19 test results, for example, are recorded and shared among health records systems, especially given how the popularity of different types of tests has ebbed and flowed. Careful abstraction of the details of validation and interpretation of these results was essential for us to balance supporting nuanced requirements from our partners without creating a system that was too complex to configure and maintain.  

Sometimes, these shifts surprised us. Early on, we spent a decent amount of time building out a data model that would accommodate sensor data for temperature scans, for example, under the assumption those would become increasingly important. As the industry instead all but abandoned them, we ended up having built out fairly comprehensive abstraction layers that proved unnecessary almost immediately.

Its important to accept upfront that there will always be a case that surprises you and requires looking under the hood.”

 

How do you typically figure out whether to keep or delete an abstraction?

Theres a famous notion that all abstractions leak, so its important to accept upfront that there will always be a case that surprises you and requires looking under the hood. That alone doesnt make an abstraction bad or unnecessary. An abstraction, to me, is worth keeping so long as the leaks remain the exception, not the rule. If debugging a slow query requires looking at the query plans now and then, that is to be expected. But the moment it becomes largely impossible to successfully use an abstraction without deep knowledge of whats happening underneath the surface, theres a good chance its outlived its utility and needs to be deleted — or at the very least reevaluated.

 

What practices can engineers use to ensure they aren’t introducing unnecessary complexity to a codebase through excessive use of abstractions? 

Three rules come to mind. One is to be careful not to design problems to meet your solutions. Some problems lend themselves to a constellation of neatly abstracted, layered systems while others can be solved with little more than a few relatively simple functions. It pays to be honest about the problem at hand.

Second: abstract organically, not artistically. That is probably the hardest for me personally. Its tempting, when first hearing about a problem, to start from a place of what abstractions faithfully represent the domain and look good on an architecture diagram, before any code is written. In reality, the most durable and useful abstractions emerge naturally from identifying undue complexity and duplication in concrete code thats already being worked on, often in surprising and unexpected places. 

Third: as a twist on the “composition-over-inheritance” concept, favor clever combinations over layered abstractions. Many services achieve their desired behavior by orchestrating interactions between pre-existing abstractions. If you find yourself writing layers that orchestrate the orchestrators, though, there's a good chance a flatter approach will be easier to maintain.

 

Image of Eric Vincent
Eric Vincent
Principal Engineer • Aetion

Aetion’s analytics enable healthcare organizations to use real-world evidence — data gathered outside of randomized controlled experiments, that is — to inform critical decisions around which treatments work best, for whom and when.    

 

Tell me about a necessary and an unnecessary abstraction you’ve encountered within your codebase.

For the sake of brevity, let us consider abstraction as it relates to inheritance in an object-oriented (OO) programming language. It is common to see solutions that employ dependency injection (DI) frameworks predicated on the use of abstractions. 

In a typical DI framework, a class depends upon an interface that abstracts away any specific implementation. During object construction, said DI framework selects and constructs an instance of a service class that implements the abstraction and injects it into the dependent class. The abstraction decouples the dependent class from the concrete implementation to shift responsibility of construction to the DI framework while allowing the DI framework to select the appropriate implementation for the condition at hand, often at run time.

As a counterpoint, one common mistake is single implementation of an abstraction, when an interface is only implemented once. This may be a case of prematurely creating a point where a system can be expanded, but often its the bad habit of creating an interface as a matter of course, also known as the cargo cult effect. Such interfaces add no value, increase complexity, reduce readability and increase maintenance cost.

 

How do you typically figure out whether to keep or delete an abstraction?

Abstractions, which occur throughout every aspect of software engineering, are about generalizing solutions by removing details. In the context of OO programming languages, the abstraction removes some — in the case of subclasses — or all — in the case of interfaces — the details of implementation, leaving only the specifics of user interaction. This increases flexibility in exchange for adding the complexity of additional artifacts to track and maintain.

The simplest litmus test is whether the benefit of the increased flexibility has enough value to outweigh the cost of the added complexity, maintenance cost, and cognitive load. The test is simple: What would a solution be without the abstraction? If removing the abstraction would create excessive proliferation of duplicate code or couple components together in an undesirable way, or even make integration with external libraries less effective or more complex, then the abstraction deserves consideration. Otherwise, simplifying by eliminating the abstraction may be the right move. 

Only as it becomes obvious where abstractions will add value should they be considered.’’

 

What practices can engineers use to ensure they aren’t introducing unnecessary complexity to a codebase through excessive use of abstractions? 

Firstly, engineers should understand the use of the libraries and frameworks their system is employing. Often these frameworks can be opinionated and force the solution to employ a particular set of abstractions. ASP.NET and Spring Boot are examples of such frameworks. Once down the rabbit hole of these frameworks, there is little choice but to play ball with how the framework authors intend for the system to be architected. This can lead to engineers developing bad habits around building unnecessary abstractions as a matter of ritual.

Aside from being driven in a direction by a heavyweight, opinionated framework, engineers should endeavor to first consider how to solve problems as directly as possible, with minimal generalization and abstraction. Only as it becomes obvious where abstractions will add value should they be considered. This could mean finding opportunities to avoid repetition, decoupling modules, opening possibilities for extension, allowing for delegation of responsibility or some combination of these conditions. This makes it especially important to make careful decisions and employ solid planning and review practices early in the design of a system.

Responses have been edited and condensed for clarity.