Hey friends 👋 Welcome to a new edition of Frontend at Scale—your biweekly newsletter for all things software design, architecture, and truly terrible dad jokes. Today we’re taking a look at one of my favorite code smells. Or should I say, we’re taking a sniff at it? Ok, I can see you shaking your head, but I did warn you about the jokes, didn’t I? Don’t worry, I promise the rest of the newsletter gets better. Sort of. Let’s dive in!
Too General Too Soon
We should talk about that smell in the air. No, not that one (what is that smell, by the way? Did I forget to take out the trash again?)
I’m talking about the code smell that comes up whenever we grab our crystal ball and try to predict the future—Speculative Generality.
Speculative generality is what happens when we (intentionally or not) write code today to solve problems we might run into tomorrow. This could mean adding extra features prematurely or making general-purpose (and reusable) abstractions that are used in just one place.
I think it’s important to talk about this code smell in particular because it happens very frequently and, if left unattended, can cause some serious damage to your codebase’s maintainability. So today I want to cover two things:
- Why speculative generality is so dangerous, and
- Why, sometimes, it can actually be a good thing.
I know it looks like I’m contradicting myself here, but please, allow me to elaborate.
The Dangers of Premature Generalization
Imagine we’re building a new dropdown component for an app we’re working on. It’s a relatively simple component (famous last words), but it has some fancy styles and animations, so it needs to be custom-built.
In addition to the dropdown’s core functionality, there are a bunch of other features we could add to it to make it more reusable and general-purpose. Some are small, like adding a new prop to disable the dropdown (even though we don’t have a disabled use case yet), and some are big, like adding multi-select capabilities.
When we sit down to implement this dropdown, we essentially have two options ahead of us: we can just solve the problem that we have right now (the dropdown’s core functionality), or we can solve some of the other problems as well, with the expectation that doing so now will save us time in the long run.
A sensible answer to this dilemma is to go with the first option—only solve the problem that we have right now. Why? Because the alternative is too risky. We’re essentially making a guess about which problems we’ll run into a few months from now. And if we’re honest, we’re not very good at predicting the future.
You might know this principle as YAGNI ("You Aren't Gonna Need It"), which warns us against premature generalizations because:
- They add a bunch of unnecessary complexity that makes the code harder to understand, and
- They often mean that we’re spending more time today building features that we don’t know if we’ll need tomorrow.
But that’s not the entire picture. In fact, YAGNI is the least of our problems here. The real danger of speculative generality is much sneakier—it hides deep under the surface and only becomes apparent once it’s too late.
To demonstrate this, let’s fast forward a few months when a new actual requirement comes in. It’s not one that we’ve prepared for, but something new. For instance, our dropdown might now need the ability to create option groups.
If we took the route of only solving the problem at hand back when we initially implemented the dropdown, adding this new feature might be straightforward. But if our starting point is the more complicated premature generalization, then trying to force it to solve the new problem might result in a particularly messy abstraction.
This is how code naturally evolves. As developers, we have a tendency to follow existing patterns in the codebase, so when new requirements come in, we’re much more likely to make the new problem fit the existing solution than to consider if there is a better way to fix the problem.
We could clean up unnecessary features, of course. But that would take time (usually in the form of a large refactor), and it might not always be obvious which parts of the code aren’t being used.
This happens more often in parts of the application that change frequently, which only exacerbates the problem. No matter which type of software you’re building right now, I bet the messier parts of your codebase are the ones that change the most—and also some of the most important in the entire application.
With all these downsides, it seems that we should avoid speculative generality like the plague, right? Well, not always. We should be careful around it, for sure, but there are cases when a little speculation can be a very good thing.
Speculative Generality vs. Optimizing for Change
You’ll notice that this “don’t try to guess the future” advice is somewhat at odds with another thing we talk about frequently in the newsletter, which is the importance of optimizing our codebase for change.
As with most things in software development, there is a tradeoff that we should evaluate before making a decision.
Avoiding premature generalizations is a good rule of thumb, but we shouldn’t follow this advice blindly. Sometimes, doing a bit of extra work ahead of time can truly save us from costly refactors in the long run.
Let’s use internationalization as an example. Imagine our app only supports one language at the moment—English, but our hot new startup is expanding to South America, so we want it to be fully translated into Spanish as well.
If we strictly follow the “never generalize” advice, we’d just do the minimum amount of work to support one extra language and no more. But it doesn’t seem that much extra effort to support an unlimited number of languages, so… should we do that instead? Here are a few questions you can ask yourself to figure that out:
- Is there a non-zero chance that we’ll need to add support for at least one more language in the future?
- What is the effort of adding support for N languages vs. adding support for just one more language?
- How much complexity does the more general solution add to the codebase? Can it be encapsulated so that developers using this feature are not exposed to this extra complexity?
Some of these questions might not have a concrete answer, and we might be required to make a series of “best guesses.” But considering these questions carefully will help us make a more informed decision about whether it’ll be more beneficial to generalize a solution early or not.
Evaluating this tradeoff can be challenging. Here are a few other tips that can help you navigate this delicate balance:
- Use the Rule of Three: in his book The Rules of Programming, Chris Zimmerman tells us that “generalization takes three examples.” Before creating a reusable abstraction that you might not need in the future, wait until you have at least three concrete use cases.
- Embrace composition: we often talk about composition from a UI-component standpoint, but you can use this principle any time you write a function. Separate your logic into small modules that follow the single responsibility principle. Composable modules can either be used independently of each other, or combined together to solve new and novel problems when they come up.
- Follow the Last Responsible Moment principle: when in doubt, ask yourself if you really need to make a decision right now about whether to generalize or not. If you can postpone this decision without any repercussions, you should do so and wait until you have more information (e.g., when you have more certainty about whether a feature will be needed or not.)
Like any other code smell, speculative generality doesn’t always indicate an actual problem. It’s important that we notice when it happens so that we can make sure we understand the tradeoffs involved, but we shouldn’t always try to “fix it.”
Creating a solution that is too general too soon can certainly be dangerous. But generalization can be used for good as well, even when done prematurely.
You’ll most certainly face this dilemma multiple times throughout your career as a frontend engineer. Making a decision is not always easy, but if you learn to recognize this smell and make a habit of evaluating its tradeoffs, you'll have everything you need to make the right call.
Links Worth Checking Out
- All this talk about generalized abstractions reminded me of tef’s classic essay on writing code that is easy to delete, not easy to extend. It’s definitely worth reading (and re-reading) every once in a while.
- Have you heard of the AHA Stack? It’s a brand new (yet somehow familiar) way of building interactive applications using Astro, HTMX, and Alpine.js.
- And since we can never have enough Astro content, here’s a guide to using Astro with Qwik that Paul Scanlon put together.
- Todd Gillies wrote a nicely illustrated guide to understanding good versus bad code that even your non-technical teammates will enjoy.
Get a Whiff of This by Sandi Metz
Speaking of code smells, Sandi Metz has a great talk about them that you should absolutely watch. You know how much I love Sandi's talks, and this one is not the exception. You’ll learn not only about the different categories of code smells, but you’ll also see a few examples of how to spot and fix them. All in just a few minutes!
I’m a big fan of Frontend Masters’ new podcast, and their latest episode with ThePrimeagen is a particularly fun one. It's a super engaging conversation that covers everything from content creation and technology trends to work-life balance and beyond. And I really mean engaging—I was late to an appointment because I lost track of time listening to it 🙃 Highly recommended! Unless you have an appointment in the next hour, in which case, maybe set up an alarm first.
That’s all for today, friends! Thank you for making it all the way to the end. If you enjoyed the newsletter, it would mean the world to me if you’d share it with your friends and coworkers. (And if you didn't enjoy it, why not share it with an enemy?)
Did someone forward this to you? First of all, tell them how awesome they are, and then consider subscribing to the newsletter to get the next issue right in your inbox.
I read and reply to all of your comments. Feel free to reach out on Twitter or reply to this email directly with any feedback or questions.
Have a great week 👋