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#.
I think you are looking for easy answers that apply 100% of the time, can find ample evidence of times when engineering effort was wasted or made code more complex than necessary, made refactoring the code more expensive than necessary, and ultimately were examples of decisions (or bad bets) that failed to pay off for the developer. But I also think you are completely ignoring all of the times those "bets" do pay off.
The counterpoint is when someone uses no forethought for what is likely to come because they are not official requirements yet. They then develop a useful subsystem that is used everywhere but in a very specific way. At some point the cost to refactor that subsystem becomes something that is never approved and either it is duplicated over and over or functionality is just bolted on. Eventually the entire code base needs to be scrapped or the company loses to a lighter competitor that made more prudent decisions. And it is rarely justifiable to scrap a code base.
In my experience, the reality is a good developer understands the pros and cons of each approach and is making a bet (sometimes called an informed decision) about a specific piece of code and keeps it as simple as possible for the likely use of the code. A bad bet leads to more expensive cost no matter which way you go. Pointing to one set of bad bets and always going the other direction isn't going to end well either.
There is not always time available for refactoring or rewriting every algorithm when your requirements change slightly.
In short, someone who is afraid of flying because sometimes planes crash might always drive instead. But it is not necessarily safer.
Hard disagree on this one. If the solution is simple, refactoring it to fit a more general use-case is going to be simple. If, on the other hand, it becomes more difficult to refactor over time, and by the time you need to extend it it's too hard, then a lot time has passed between writing the initial solution to the "extension". If a lot of time has passed, then the chance a future use-case will fit whatever infra you prepared is pretty small. And all that time you had to maintain a more generic custom solution.
Sure, and the answer is always "It depends". I don't contradict that every case is different and context is king.
So the answer is to write an algorithm that fits all cases?
I don't understand the reference. I'm just suggesting not to prepare a plain "just in case", when you currently need a car.
Oh! I love the Fizzbuzz "Enterprise Edition" version. Btw I became aware of this joke (or so I hope) repo a few months ago during the raging discussions between Casey Muratori and Uncle Bob (Have a look at https://github.com/unclebob/cmuratori-discussion/blob/main/cleancodeqa.md if you have some time to spare): really a clash of philosophies! Personnally, being a C# dev and having been fed with Enterprise Architecture, Design Patterns and Object Design dogmas, I know I have a natural tendency to overuse generics, interfaces or factories (not inheritance though). But I try to cure myself. Looking at what people like Muratori (or more generally coming from the Games industry) say or looking at recent languages such as Zig helps broaden the horizon and question the real usefulness of all this infrastructure code we produce.
PS: Heartedly with you, wishing a magic wand could instantly solve what's going on in Israel! Keep up the good work!
Thanks for the well wishes Olivier! That discussion is way too long for me, but everything uncle bob writes is interesting, even if I don't always agree. I think we were all guilty of premature infrastructure at one time or another. I'm not familiar with Zig. Can you give an example how it changed your point of view? Is it more towards the functional side where you don't have to keep state?
Another variation of this is server infrastructure like Redis or ElasticSearch. The lead dev for a project I worked on felt it was necessary to have these two things, that Postgres is not fast enough for a realtime application, or for backing a search box that shows results as the user types.
The realtime part is simply that if users Bob and Janet are logged in, and looking at the same screen, any changes Bob makes automatically appear on Janet's screen, and vice-versa. Kind of like social media apps.
Realistically, Postgres can return a random row out of a million in 2ms on your average middle of the road desktop or laptop. The system being developed would be used by about 100 users, who would create about 10,000 records per year. It would literally take a century at that rate to generate a million rows.
In such a scenario, Postgres is absolutely good enough by itself. Redis still has network delays, so cannot significantly improve on Postgres. ElasticSearch has a complex query language, so it must have a planner, which I doubt is somehow really superior to that of Postgres.
It is perfectly fine with such a pithy amount of data to bang away on Postgres queries for the filter box, and to handle real time communication. Users would not see any difference.
Initially, unit tests tested db queries, and an interface was made for the needed Redis functions. After awhile, people just stopped bothering to keep with testing Redis anymore in services, because they didn't see any value in it, and it was a bit annoying to test it.
The basic querty process was:
You can see how annoying it is to test all the parts of that for every single query in every single microservice. This was compounded by the fact that nobody ever bothered to even try to make an abstraction that would handle those details automatically, where you might just provide a key function and a query function to the abstraction.
Every server infrastructure is like a child, it is the gift just keeps on taking. Newer versions need to be used to avoid security problems, bugs around its usage have to be corrected, changes in the interface require coding changes, someone has to keep it running from day to day, someone has to investigate why it is running but not returning the expected data.
Server infrastructure should be added as needed, and only used where needed, to only solve actual experienced, measurable problems. It should not be used just to meet preconceptions.
Yeah I feel your pain. Unnecessary caching hurts.
I experienced something similar as well. When working on UI frameworks (WPF), some devs would optimize button-click handlers for no reason. For example, they'd make something to run for 1millisecond instead of 20 milliseconds, changing to a really complex solution. I mean, great work man, but for the user 20 milliseconds is just like 1 millisecond.
Many good points made here.
But be careful-right now I'm dealing with a code project that is the opposite extreme. Written in VBA, it looks like the developer didn't know what a design pattern was.
My theory is: Use every design pattern that is applicable, but as you said, limit the scope to the current KNOWN issues. A good program is one that implements good design patterns to solve the intended problem. Such a program can then be easily extended as needed.
Yeah, don't have anything against design patterns, just against overusing them. Those patterns are great for many things. One advantage is that they create a shared vocabulary. e.g when you make a "Factory" or a "Singleton" method, everyone knows right away what this code is meant to do.