What 13 Years of Web Development Taught Me About Simplicity

March 5, 20266 min read
careersoftware-engineeringsimplicityarchitectureleadership

In 2013, I shipped my first production code. It was jQuery spaghetti — event handlers bound to DOM elements by ID, AJAX calls nested three levels deep, a utils.js file that had grown to 2,000 lines because nobody knew where else to put things. It worked. Users didn't care how it was built.

Thirteen years later, I've worked with Angular, React, Next.js, Node.js, and more frameworks than I can remember. I've led teams, architected platforms, and migrated systems at scale. And the single most valuable skill I've developed isn't mastering any particular technology. It's knowing when to stop adding things.

The Framework Treadmill

Every few years, the industry decides the current way of building things is wrong and invents a new one. I've lived through several cycles:

jQuery taught me that manipulating the DOM directly is powerful and dangerous. You could do anything, which meant you eventually did everything, and nobody could untangle it.

Angular (the original AngularJS) taught me that structure matters. Two-way data binding felt like magic until your application grew and you spent hours tracing why a value changed. The lesson: magic is just complexity you haven't debugged yet.

React taught me that unidirectional data flow and component composition solve real problems. But it also brought its own complexity — state management libraries, build tooling, the endless debate about class components versus hooks versus server components.

Next.js taught me that the framework can handle the boring parts — routing, code splitting, server-side rendering — so you can focus on the product. But only if you resist the urge to fight the framework's opinions.

Each transition forced me to throw away knowledge I'd invested in. That's uncomfortable. But it also taught me to look for what survives across frameworks: clear data flow, small composable units, separation of concerns, and the ability for a new team member to understand the code without a guided tour.

The Complexity Trap

Mid-career engineers — and I include my past self — have a dangerous tendency: we over-engineer because we can. We've learned patterns, we've read the books, and we want to use what we know.

I've written abstraction layers "just in case" the database changes. Built plugin systems for applications that would only ever have one plugin. Created configuration-driven architectures for things that could have been a simple if statement.

The justification is always the same: "what if we need to change this later?" And the answer, almost always, is: you won't. And if you do, the abstraction you built today won't match the change you need tomorrow, because you can't predict the future. You've added complexity for a scenario that never arrives, and now every developer who touches the code has to understand your abstraction layer before they can do anything useful.

The hardest lesson in software engineering isn't learning how to build complex systems. It's learning when not to.

Where Simplicity Actually Won

Let me give you real examples, because "keep it simple" is easy to say and hard to believe without evidence.

Replacing 120 WordPress sites with one tool. I wrote about this in a previous post. The short version: we had over 120 WordPress installations for what were essentially static marketing pages. The "complex" approach would have been to build a better WordPress management platform — orchestration, automated updates, centralized monitoring. The simple approach was to ask: do these pages actually need WordPress? They didn't. A page builder that outputs static HTML, deployed to a CDN, replaced all of them. Fewer moving parts, lower costs, faster delivery.

Running a homelab on Raspberry Pis. I could run my personal infrastructure on cloud services. I'd get managed databases, auto-scaling, monitoring dashboards out of the box. Instead, I run everything on three ARM boards with Docker and Ansible. It's simpler in the ways that matter: I understand every component, I control every configuration, and the monthly cost is my electricity bill. Simple doesn't mean easy — it took work to set up. But the result is a system I can hold in my head entirely.

Cutting a Campaign Manager's load time from 20 seconds to 1 second. This one surprised me the most. I inherited a legacy application that took 20 seconds to load its main view. The previous team had tried adding caching layers, optimizing database queries, and introducing a CDN. All valid approaches, all adding complexity. When I actually profiled the application, the problem was simpler than anyone expected: the frontend was loading megabytes of data it didn't need, running layout calculations on thousands of DOM elements that weren't visible, and blocking the main thread with synchronous operations that should have been deferred. The fix wasn't adding anything. It was removing: lazy loading, virtual scrolling, code splitting, and cleaning up dead code. The performance improvement came from doing less, not doing more.

What "Simple" Actually Means

I want to be precise about this because "simple" gets misused.

Simple doesn't mean easy. Building a simple system often requires more thought than building a complex one. You have to understand the problem deeply enough to know what to leave out.

Simple doesn't mean lazy. It's not about cutting corners or skipping error handling. A simple system handles failures gracefully — it just doesn't handle hypothetical failures that the architect imagined at 2 AM.

Simple means intentional. Every component exists for a reason you can articulate. Every abstraction solves a problem you actually have. Every dependency earns its place.

Simple means maintainable. A new team member can read the code and understand what it does without needing tribal knowledge. The debugging path from "something is wrong" to "here's the cause" is short.

Simple means transparent. When something breaks — and it will — you can see why. The system doesn't hide its behavior behind layers of indirection.

Teaching Forces Simplicity

One thing that accelerated my appreciation for simplicity was mentoring junior and mid-level engineers. When you have to explain a system to someone who didn't build it, you quickly discover which parts are essential and which parts are accidental complexity you never cleaned up.

I've had the experience of drawing an architecture diagram for a mentee and realizing, mid-explanation, that a component I built doesn't need to exist. If you can't explain why something is there in two sentences, it probably shouldn't be.

Mentoring is a mirror. It shows you your own blind spots. And the most common blind spot for experienced engineers is complexity we've normalized — things we stopped questioning because we built them and they work. Working isn't the same as simple.

Advice for Mid-Career Engineers

If you're five to ten years into your career, here's what I'd tell you:

Stop chasing frameworks. Learn one well. Understand its trade-offs. The next one will come, and your transferable skills — state management, component design, data flow, testing strategies — will matter more than framework-specific knowledge.

Question inherited complexity. When you join a project and see something complicated, don't assume it's complicated for a good reason. Ask. Often, the answer is "nobody remembers why we did it this way."

Measure before optimizing. The Campaign Manager story above is a perfect example. Three teams tried to optimize the wrong thing. Profiling first would have saved months.

Write code for the reader, not the writer. Your clever one-liner isn't clever when someone else debugs it at midnight. Clarity beats brevity every time.

Delete more than you add. The best pull requests I've reviewed removed more lines than they added. If you can solve a problem by removing code, you've found the right solution.

Thirteen years in, I'm still learning. But the direction has changed. Early in my career, I learned by adding: new languages, new frameworks, new patterns. Now I learn by removing: unnecessary abstractions, premature optimizations, assumptions about what the code might need someday.

The best code I write today is the code I decide not to write at all.