Hey friends đź‘‹ Welcome to another edition of Frontend at Scale, your bi-weekly newsletter for all things software design, architecture, and analogies with increasingly older video games which I'm realizing now are definitely showing my age.
Today's essay is a slightly adapted version of an article a younger and more energetic Maxi wrote in June of 2022. This was about a year before I started this newsletter, but as you'll see, the concepts of essential and accidental complexity were already living rent-free in my head.
We'll also chat about Remix vs. React Router, SPAs vs. MPAs, writes vs. write-nots, and cage-free vs. free-range eggs. Okay, we're not actually talking about that last one but whoever is in charge of naming egg types, please change the name of one of these so that my wife stops getting mad at me when I inevitably buy the wrong kind at the grocery store.
Alright, let's dive in.
SOFTWARE DESIGN
A Losing Game
If you’ve ever played a classic RPG video game like Final Fantasy or Dragon Quest, you might have encountered what is known as a Hopeless Boss Battle. This is a mechanic typically used for the sake of the game's plot, in which our heroes have to face an impossibly powerful enemy in a battle they’re supposed to lose.
Building software is very much like one of those battles, except that our enemy is not a level 9,000 dark knight or a Kraken-like monster with an impenetrable shield. Our battle is against a much scarier beast: complexity.
Complexity is the single major problem in software development. It is what makes systems hard to understand, maintain, and modify, and it’s the main cause of software products being delivered late or not at all.
In their classic paper Out of the Tar Pit, authors Ben Moseley and Peter Marks rightfully call the enemy by its name:
Complexity is the root cause of the vast majority of problems with software today. Unreliability, late delivery, lack of security — often even poor performance in large-scale systems can all be seen as deriving ultimately from unmanageable complexity.
As an eternal optimist, there was a time in my career when I was certain I could defeat complexity.
I felt that if I just had enough time and resources, I could design and implement a perfect version of my software that would be completely free of bugs and edge cases. I thought that if I could just refactor all the legacy code and handle every piece of tech debt, I would finally be free of the curse of complexity and my productivity would be through the roof!
But I was wrong. As most seasoned developers know very well, perfect codebases don’t exist because complexity always finds a way to creep into our projects. There are always tradeoffs to make, changes to adapt to, and real, complex problems to solve. Even the simplest design we could possibly think of will eventually fail to keep complexity out of our codebases.
So the first step in the battle against complexity is to admit defeat. If that sounds depressing is because, well, it kinda is. But it's also quite liberating—if the perfect design doesn’t exist, then there’s no pressure to create it.
We've Already Lost
Fighting complexity is a hopeless battle because any piece of useful software comes with a certain amount of essential complexity attached to it, which is the complexity of the problems it solves and the domain it lives in. This type of complexity is the entire reason the software exists in the first place, so there is really no way around it.
We do have some level of control over the accidental complexity of our applications, however, which is the complexity of the tools and patterns we use to build them. But while this type of complexity is more manageable, it is also inevitable.
As time, frequency of changes, and the number of contributors to the codebase increase, so does its accidental complexity. We can manage and encapsulate it, but we can never completely eliminate it.
Complexity in a codebase builds slowly over time with every line of code we write and every tiny decision we make. In the early stages of a project, these tiny bits of complexity might seem harmless. A little code smell here, a small broken window there, a few “I know this is a hack but I don’t have time to fix it right now” here and there.
But the most dangerous thing about complexity is that it's exponential—these tiny bits add up, and before we know it, the clean and maintainable codebase we were once so proud of has turned into a piece of legacy code we just can't wait to refactor.
Okay, ok. I know this is a bit of a downer, so let me reiterate the good news: our job as software engineers is not really to defeat complexity but to manage it—to put the beast to sleep as long as we possibly can so that we can build software that lasts longer.
My favorite book on this subject is A Philosophy of Software Design by Professor John Ousterhout. The book is not only packed with clear, practical advice for dealing with complexity, but it also states the importance of our role as software designers right at the beginning of chapter 1:
If we want to make it easier to write software, so that we can build more powerful systems more cheaply, we must find ways to make software simpler. Complexity will still increase over time, in spite of our best efforts, but simpler designs allow us to build larger and more powerful systems before complexity becomes overwhelming.
The fight against complexity might be an impossible one, but it is one we must fight anyway. If complexity is the most fundamental issue in software development, then managing it by making software simpler must be the most important part of our jobs.
Of course, simplicity is hard. Coming up with a simple design is a lot harder than building a complex one, and the fact that software requirements are constantly changing doesn't make our jobs any easier.
But just like the heroes of a Role-Playing Game facing an unwinnable battle, we can’t just give up. If we want to stand a chance at building great software, we must rise to the challenge, look directly into the eyes of complexity, and fight it with all our might.
ARCHITECTURE SNACKS
Links Worth Checking Out
- Mark Dalgleish from the Remix team gave a great talk at React Advanced telling the story of how React Router became a framework. You might have heard that Remix is merging into React Router and you might have questions like… what does that even mean? Well, look no further than this talk to answer all of your burning questions about it. The TLDR is that the latest version of React Router (v7) continues the library we know and trust, but it now comes with a Vite plugin that transforms it into a full-blown framework. Pretty exciting stuff.
- Jake Lazaroff is cementing his reputation as the quadrant chart guy with a great answer to the question what is a single-page application? If you're wondering what the main differences between apps built with Next.js, Astro, and htmx are, this article's got you covered.
- I just learned that Zach Tellman is writing a book on software design and this excerpt about the death of the architect got me pretty excited about it. It's a short essay about the evolution of the traditional software architect role and the ideas that led to its demise in the post-waterfall era.
- Corbin Crutchley wrote a nicely illustrated article explaining what Signals are and why they're taking over the frontend development world.
- I enjoyed this blog post by engineering manager Candost Dagdeviren on the traits that make a good software engineer. I naively thought "writes code" would be at the top of the list, but I guess there's a bit more to it.
- Reading writing advice is my favorite way to procrastinate on actually doing the writing myself, and this article by Robin Moffatt on blog writing for developers was my pick this past week. It also includes a bunch of resources at the end which is great because I now have enough content to procrastinate for the rest of the year.
- Speaking of writing, this short Paul Graham essay on writes and write-nots is all the motivation you need to continue or start writing yourself, even if AI can do most of the work for you these days.
- If your website could use a performance boost, check out this article on Google's web.dev with the most effective ways to improve Core Web Vitals. I like this list because it's very practical and can take you 80% of the way with just 20% of the
headacheeffort. - Eric Bailey wrote about making content-aware components with CSS and... look, I'm not gonna lie—a lot of these new CSS techniques are going completely over my head, but it's really cool to see how much we can do these days without getting JavaScript involved.
- Paul Hebert wrote a cool tutorial on how to generate random mazes with JavaScript. Will I ever need to use this in a real application? Probably not. But was it fun learning about it? Absolutely.
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, LinkedIn, or reply to this email directly with any feedback or questions.
Have a great week đź‘‹
– Maxi