Premature infrastructure is a peculiar behavior pattern that I witnessed in every single tech company I worked for. It is the habit of creating infrastructure code before it is actually needed. The development team is predicting future requirements and preparing ahead of time. That might be preparation for a future feature, extension capabilities that aren’t needed yet, or customization that may or may not be wanted.
I believe that creating premature infrastructures is one of the biggest problems in software development. It’s similar to the sentiment of the original quote premature optimization is the root of all evil. Like with premature optimization, premature infrastructure introduces more code, more complexity, and more unnecessary abstractions that make development more difficult. More often than not, the predictions aren’t fulfilled, and this infrastructure is never used.
We can make the distinction between two types of preparations: micro-preparations and macro-preparations.
Micro preparations
Micro preparations are small design choices that aren’t necessary for the current functionality, but pave the way for future developments [that the developer thinks will be needed]. Here are some examples:
-
Creating interfaces unnecessarily. Interfaces are great for two things: to allow different implementations and to create an easy way to mock something in tests. If you’re not using one of those, i.e. you’re implementing a single class from an interface and you’re not mocking that interface in tests, then you don’t need an interface. A common argument for creating an unnecessary interfaces is that they will allow to easily add another implementation. My counter-argument is that if and when the time comes, it’s very easy with modern IDEs to refactor and extract an interface.
There is an exception to this rule. When you create an interface in a framework, the interface acts as a contract and it’s not unnecessary, even if there’s just 1 or 0 implementations.
-
Trying to make generic functionality when you don’t have to. There’s a certain beauty in functions or classes that can be flexible enough to serve multiple purposes. For example, generic collections like
List<T>
can be reused for different types. Developers might predict, or wish, that their class will be used for other use cases. That’s whyWiFiManager
becomesInternetProviderManager<T>
withT
beingWiFi
. Or whyPrintPDF
can change toPrint<T>
withT
beingPDF
. In my nightmares,PrintPDF
can even transform to something likeExportToOutputDevice<TDevice<TPayload>>
. -
Making unnecessary base classes. Like with generic functionality, some developers love a good inheritance structure. Given a requirement to show a progress bar UI control, they might create 3 classes instead of one:
ProgressBar
which derives fromProgressIndicator
, which in turn derives fromBaseIndicator
. Those have their place in UI frameworks, but not in an app with a singleProgressBar
control. -
Creating design patterns for configuration and extensibility when you have a single use case. Some engineers fall in love with design patterns, mostly after reading a book about design patterns. The result is trying to fit as many patterns as possible in their code. Unnecessary uses of the factory pattern, visitor pattern, or decorator pattern are just a few of the cases I saw.
If you want a good example of what I mean, check out the FizzBuzzEnterpriseEdition repo . It implements the programming challenge of the game FizzBuzz which requires going over numbers 1 through 100. If the number is divisible by 3 print Fizz; if the number is divisible by 5 print Buzz; if the number is divisible by both 3 and 5, print FizzBuzz. Otherwise, print the number.
Instead of solving this challenge with a few lines of code, this project tries to solve it in a configurable and extensible matter. Here’s a snippet to give you an idea of the madness:
public final class LoopContext implements LoopContextStateManipulation, LoopContextStateRetrieval {
private final LoopInitializer myLoopInitializer;
private final LoopFinalizer myLoopFinalizer;
private final LoopCondition myLoopCondition;
private final LoopStep myLoopStep;
private int myCurrentControlParameterValue;
/**
* @param nLoopControlParameterFinalValue int
*/
public LoopContext(final int nLoopControlParameterFinalValue) {
super();
final ApplicationContext context = new ClassPathXmlApplicationContext(Constants.SPRING_XML);
final LoopComponentFactory myLoopComponentFactory = context.getBean(Constants.LOOP_COMPONENT_FACTORY,
LoopComponentFactory.class);
this.myLoopInitializer = myLoopComponentFactory.createLoopInitializer();
this.myLoopFinalizer = myLoopComponentFactory.createLoopFinalizer(nLoopControlParameterFinalValue);
this.myLoopCondition = myLoopComponentFactory.createLoopCondition();
this.myLoopStep = myLoopComponentFactory.createLoopStep();
((ConfigurableApplicationContext) context).close();
}
Macro preparations
While micro preparations are a small evil, macro preparations are a big evil. That’s where you’re preparing the entire system for a future that may or may not come. A future that’s not a current requirement, but for some reason, you’re confident about what’s going to be the next feature and you want to create a proper design for it. The best way to illustrate what I mean is with an example.
YouTune: A cautionary tale
Let’s say that you’re creating a music streaming app called YouTune. A new startup that will compete with the likes of Spotify and Apple Music. Your first big feature is a “Liked Songs” functionality. The users will “like” their favorite songs, which will appear in a “Liked Songs” playlist.
The development team wants to do the best possible job, so they create a generic design that can accommodate future playlists, like a specific artist playlist, or a discovery playlist that’s created by an algorithm. They make the following IPlaylist
interface and a BasePlaylist
class.
IPlaylist
{
IList<Song> Songs;
string VoiceAssistantCommand;
AddSong();
RemoveSong();
Shuffle();
Play();
Pause();
NextSong();
PrevSong();
}
Although there’s a single implementation of IPlaylist
called LikedSongsPlaylist
, the entire application is built to work with multiple playlists. After all, you predict there will be many other types of playlists like ArtistPlaylist
and a WeeklyDiscoverPlaylist
. The team even creates generic screens that can handle many playlists. It takes longer, but now future developers will be able to easily add more types of playlists and everything will just work.
As it happens, the music business doesn’t go as planned. After a few months, the application isn’t able to scale. The board of directors decides that the best course of action will be to pivot to podcasts. Podcasts are really trending now and there’s no need to pay royalties to artists. The startup is officially pivoting to a podcast player. As it happens, the IPlaylist
interface doesn’t suit podcasts. It’s fitted for songs. For example, there’s no “Shuffle” functionality in podcasts, and there’s need for fast-forward and rewind-back instead of “NextSong” and “PrevSong”. Not to mention that all the screens are built for playlists, not a queue of podcast episodes.
Unfortunately, the development team took so long to develop the initial playlist feature that there was no money left to spend on podcasts. The startup tries to raise more money, but without success. Everyone gets fired. The startup’s failure becomes notorious, and none of the employees are able to find jobs in tech ever again.
See, that’s how bad over-engineering can end. The moral of the story is that you shouldn’t try to guess the future. The right thing to do is the best and simplest thing for the current requirements. Or for future known requirements. We should develop following the principles YAGNI (You aren’t going to need it) and KISS (Keep it simple, stupid).
When new requirements come, you might need to change the existing design to fit the new current requirements. That means refactoring should be an ongoing part of the development process, as Martin Fowler recommends in his book Refactoring .
Final thoughts
Premature infrastructure is a form of predicting the future. Engineers assume they know which way a program is going to evolve using common sense, but from my experience software requirements are rarely what you expect. More often than not, requirements will turn development in an entirely different direction than what you assumed. The created infrastructure often becomes irrelevant, adding complexity or bugs. Other times, it needs to be changed or scrubbed entirely.
This kind of phenomenon seems to follow me everywhere in my career. Every company has strong-willed engineers that have a special place in their hearts for unnecessary abstractions, prolific use of design patterns, and making everything extensible in advance. I understand they mean well, but the results are destructive.
I think this behavior pattern and similar topics are problems that we don’t discuss enough as a community. Design paradigms and behavior patterns are important, and we should probably talk more about them and less about comparing frameworks, debugging tricks, and arguing which programming language is better. Which is obviously C#.