CCAB
Home/Posts/Choosing the Stack
08 September 2020 · Post

Choosing the Stack

Frameworks superstars - The Paradox of the Stack: How We Solved Simplicity by Making Everything Complex

Rollercoster on the beach boardwalk, image from Carlo Alberto Burato

A Thought Experiment in Reverse Engineering the Web

Let's start with a radical thought experiment. Strip everything away. No frameworks. No build tools. No package managers. No CMS. No database. No server-side language. Just HTML files, written by hand, one page at a time. A return to the absolute origin of web development — the way sites were built in 1993, when the web was a network of documents and nothing more.

What would happen?

The first thing you'd notice is speed. Astonishing, almost absurd speed. A hand-coded HTML file is just text. A few kilobytes of markup, served directly from disk. No database query. No template engine parsing variables. No middleware pipeline processing the request through seventeen layers of abstraction. The browser requests a file, the server returns it, and the page renders. The entire transaction completes in milliseconds. Performance, in the purest sense of the word, is essentially solved.

The second thing you'd notice is resilience. There's almost nothing that can break. No database connection to drop. No ORM to misconfigure. No dependency conflict to debug. No runtime error in a server-side language to trace through a stack of framework internals. The failure modes of a static HTML site are limited to the file being missing or the network being down. That's it. The surface area for technical problems shrinks to nearly zero.

For a moment, this feels like freedom.

The Collapse of the Handmade Web

But that moment doesn't last. Because the instant your site grows beyond a handful of pages, the experience reverses completely.

Imagine maintaining a hundred pages of hand-coded HTML. Now imagine a thousand. Every page shares a navigation bar — and when you add a new section, you need to update that navigation in every single file. The footer contains a copyright year — and on January 1st, you're editing a thousand documents to change a four-digit number. A client wants to update a blog post, but they don't know HTML, and even if they did, they'd need FTP access to the server and the confidence to edit a file without accidentally deleting a closing tag that collapses the entire layout.

Content management becomes a logistical nightmare. Collaboration becomes effectively impossible. The act of updating a website transforms from a creative task into an exercise in clerical endurance. The site might be fast, but it's frozen — too painful to change, too brittle to grow, too hostile to anyone who isn't the original developer.

This is the fundamental tension that has driven the entire history of web development: the things that make a site fast and simple to serve are the same things that make it slow and painful to maintain.

The Bargain We Made

Content Management Systems emerged as the resolution to this tension, and their logic was compelling. Instead of writing content directly into HTML files, store it in a database. Instead of maintaining a hundred separate pages, create templates that pull content dynamically and assemble pages on the fly. Instead of requiring developers to make every text change, give editors a web-based interface where they can write, edit, and publish without touching code.

The bargain was explicit: trade some performance for a massive gain in manageability.

But the bargain came with terms that weren't immediately obvious. A CMS needs a database — MySQL, PostgreSQL, MongoDB, whatever the era preferred. A database needs a server-side language to communicate with it — PHP, Java, ASP.NET, Python, Ruby. That server-side language needs a runtime environment. The runtime environment needs an operating system, configured and secured. The operating system needs hardware, provisioned and maintained. Each layer adds complexity, and each layer adds potential points of failure.

What started as "we need an easier way to update text on a web page" cascaded into an entire infrastructure stack. You couldn't just write HTML anymore. You needed to choose a database engine, a server-side language, a hosting environment, a deployment strategy, a caching layer, a backup system, and — eventually — a team of people who understood all of these things and how they interacted.

The simple act of publishing a sentence on the internet now required an orchestrated system of interdependent technologies.

Two Decades of Musical Chairs

For roughly twenty years — from the late 1990s through the mid-2010s — the web development industry cycled through an extraordinary number of variations on this theme. The CMS landscape alone is a graveyard of ambition: WordPress, Joomla, Drupal, Mambo, Typo3, ExpressionEngine, Textpattern, Movable Type, concrete5, Kentico, Sitecore, Adobe Experience Manager — open source and proprietary, simple and enterprise, each promising to be the one that finally got the balance right between power and usability.

Server-side languages followed their own arc. Perl gave way to PHP, which competed with ASP, which evolved into ASP.NET, which shared the stage with Java servlets and JSP, which were challenged by Ruby on Rails, which inspired Django in Python, which ran alongside Node.js when JavaScript made its improbable leap to the server. Each generation carried lessons from the last, but also carried new opinions about how the stack should be organized.

The driving forces behind all this churn were consistent, even if the solutions kept changing. First, performance — the web moved from dial-up to broadband to 4G to 5G, and at each transition, user expectations for speed reset upward. Every new connection generation exposed the sluggishness of the previous architecture. Second, user experience — as browsers became more capable and mobile devices became dominant, the gap between what users expected and what server-rendered pages could deliver widened into a canyon that demanded new approaches.

But here's the consequence that nobody planned for: with every iteration, the coupling between frontend and backend tightened.

The Era of Entanglement

In the classic CMS era, the server-side language wasn't just handling data — it was embedded directly in the presentation layer. PHP files mixed HTML markup with database queries and control logic in the same document. ASP.NET pages interleaved C# code blocks with HTML elements. JSP files wove Java expressions into the markup. The "view" and the "logic" didn't just coexist; they were physically intertwined, character by character, line by line.

This coupling had a cascading effect on every aspect of development. Choosing your stack didn't mean selecting independent tools for independent problems. It meant making a single, monolithic decision that determined nearly everything. If you chose PHP, you were choosing MySQL (usually), Apache (probably), and Linux (almost certainly) — the famous LAMP stack. If you chose ASP.NET, you were choosing SQL Server and IIS and Windows. If you chose Java, you were choosing the entire J2EE ecosystem with its application servers and XML configuration files and enterprise deployment rituals.

In this world, JavaScript was an afterthought. It handled dropdown menus, form validation, image sliders — the cosmetic interactivity that the server couldn't provide. It was the DOM manipulation script, nothing more. Nobody chose a stack based on their JavaScript strategy because JavaScript wasn't a strategic technology. It was a utility.

The frontend developer, to the extent that role even existed as a distinct discipline, was a passenger in someone else's architectural decision.

The Divorce

Then JavaScript grew up. And everything changed.

The trajectory is well documented but still remarkable in its speed. jQuery normalized browser inconsistencies and made DOM manipulation accessible. Backbone.js proved that JavaScript could manage application state. AngularJS demonstrated that entire applications could run in the browser. React reimagined the UI as a function of state. Vue offered the same power with a gentler learning curve. Node.js proved that JavaScript could run on the server, breaking the last conceptual barrier that had confined it to the browser.

In the span of roughly a decade, JavaScript went from a scripting language that "real" developers looked down on to the most widely deployed programming language in history, capable of handling everything from mobile interfaces to server APIs to machine learning pipelines.

The result was a decisive separation — a divorce, in the most architectural sense. Backend and frontend split into independent systems that communicated through a narrow, well-defined channel: the web API. REST endpoints. GraphQL schemas. The contract between the two sides became explicit and formalized: the backend exposes data at these URLs, in this format; the frontend consumes that data and renders it however it sees fit.

This was, by most measures, a genuine improvement. Teams could now work in parallel. The backend could be rewritten without touching the frontend. The frontend could be redesigned without modifying a single server-side file. Each side could be deployed on its own schedule, scaled independently, maintained by specialists in their respective domains.

Developers began choosing their stacks as two separate decisions: which technology for the backend endpoints, and which JavaScript framework for the frontend. These decisions could be made independently, by different people, with different priorities. The coupling that had defined the previous era was finally broken.

This was, I believe, a good way to go.

The Inversion: When the Stack Chooses the Developer

But liberation has a way of producing its own constraints, and the framework era introduced a subtle inversion that the industry has been slow to recognize.

Frameworks became popular because they solved real problems. Bootstrap gave teams a consistent design system and responsive grid without requiring deep CSS expertise. React gave developers a mental model for building complex, interactive interfaces that would have been nearly impossible to manage with vanilla JavaScript. Vue lowered the barrier to entry for component-based architecture. Angular provided an opinionated, complete solution for enterprise-scale applications. Each framework earned its popularity by making difficult things manageable and tedious things automated.

But somewhere along the way, the relationship between developer and tool reversed polarity.

In the early days, the decision process was architectural: what does this project need? What are the constraints? What are the user requirements? The answers to those questions led to a technology choice. The project's needs drove the stack.

Today, the process frequently runs in the opposite direction. A team starts a new project and the first question isn't "what does this application need to do?" but "which framework should we use?" And the answer to that question is almost always determined by a combination of factors that have nothing to do with the project itself: which framework is most popular right now? Which one has the most job postings? Which one will look best on a résumé? Which one did the team lead use on their last project? Which one has the most active community, the most npm downloads, the most GitHub stars?

The framework's popularity at the moment the project begins has become more influential than the project's own requirements. The persona research, the content architecture, the performance constraints, the user needs — all of these are, in practice, subordinated to a technology decision that was made before the problem was fully understood.

This is the inversion: the stack is choosing the developer, not the other way around.

The Hamburger Menu Problem

The consequences of this inversion range from the merely wasteful to the genuinely harmful, and they're visible at every scale.

At the smallest scale, consider this scenario, which plays out on thousands of projects every year: a team needs to build a marketing site. The site has five pages. It needs to look polished on mobile. The most complex interactive element is a hamburger menu — a navigation toggle that opens and closes a menu panel.

A hamburger menu is, at its core, about forty lines of CSS and maybe fifteen lines of vanilla JavaScript. A click handler toggles a class. A CSS transition animates the panel. An event listener closes it when the user clicks outside. That's the entire scope of the problem.

But the team doesn't write forty lines of CSS and fifteen lines of JavaScript. They install React. React requires a build system — Webpack or Vite. The build system requires configuration. React encourages component-based architecture, so the five-page marketing site is decomposed into components: a Header component, a Navigation component, a MobileMenu component, a MenuItem component. State management enters the picture — useState for the menu toggle, maybe useEffect for the click-outside handler. The development server needs to be configured. The production build needs to be optimized. The bundle needs to be analyzed to ensure the site isn't shipping 200 kilobytes of JavaScript to toggle a menu that CSS could handle alone.

The team has deployed an industrial-strength UI library to solve a problem that predates React by fifteen years. Not because the project required it, but because React is what the team knows, and reaching for a framework feels safer than writing raw code, even when the raw code would be simpler, faster, and more appropriate.

This pattern repeats at larger scales and with more damaging consequences. Applications are architected around framework capabilities rather than user needs. Database schemas are designed to match ORM conventions rather than domain models. Deployment strategies are chosen based on framework documentation rather than traffic patterns. The tail wags the dog at every level of the stack.

The Cost of Not Asking Why

The deeper problem isn't that frameworks are bad — they aren't. Bootstrap democratized responsive design. React genuinely revolutionized how we think about UI state. Vue made component architecture accessible to developers who found Angular's learning curve punishing. These tools earned their dominance by solving hard problems well.

The problem is that the industry has developed a reflexive dependency on frameworks that bypasses the most important question in engineering: is this the right tool for this specific job?

Every framework carries weight. Conceptual weight — the mental model you need to internalize before you can be productive. Dependency weight — the packages your project now relies on, each with its own maintenance trajectory and security surface. Bundle weight — the kilobytes or megabytes of code shipped to the client. Organizational weight — the hiring constraints, the onboarding time, the upgrade cycles, the breaking changes between major versions.

That weight is justified when the framework's capabilities align with the project's actual requirements. It's not justified when the framework is chosen out of habit, popularity, or the assumption that every project needs the same tools.

The lost art, the skill that the framework era has quietly eroded, is the ability to look at a problem and determine the minimum viable technology to solve it. To ask: does this need a framework, or does it need thirty lines of well-written code? Does this need a database, or does it need a JSON file? Does this need a deployment pipeline, or does it need an FTP upload?

These aren't nostalgic questions. They're engineering questions. And the teams that still ask them — that still match tools to problems rather than problems to tools — consistently ship faster, maintain more easily, and build things that perform better under real-world conditions.

Full Circle

There is an irony in the arc of web development history that's worth sitting with.

We started with hand-coded HTML files: fast, simple, impossible to maintain. We added databases and server-side languages to make maintenance feasible, and in doing so created complex, tightly coupled systems that were slow and fragile. We spent two decades trying to decouple those systems — MVC, REST, SPAs, microservices, headless CMS, JAMstack — and each step reduced the coupling but added new layers of tooling. Now, with static site generators and pre-rendered pages, we've arrived at something that looks remarkably like where we started: HTML files, served from a CDN, rendering instantly in the browser.

The difference is that the HTML files are now generated by sophisticated build processes that handle the content management problem the early web couldn't solve. The simplicity is engineered, not primitive. But the end result — the thing the browser actually receives and renders — is, in many cases, the same thing it would have received in 1995: a static HTML document.

We traveled an enormous distance to arrive back at the beginning. The question worth asking, as we start the next chapter of this history, is whether we've learned enough from the journey to resist the temptation of unnecessary complexity the next time around.

Based on the industry's track record, I wouldn't bet on it. But I'd very much like to be wrong.

Photo by Carlo Alberto Burato