The Difference Between Good Code and Bad Code Is 5 Minutes
I’m a fan of the KISS principle. Keep it simple. That applies when you’re building something new, but it applies just as much when you’re fixing something old.
Across every role I’ve had, agency work, staff engineer at a fintech startup, time at AWS and Meta, consulting for startups, building my own products, the pattern is always the same. The difference between code that’s maintainable and code that becomes a nightmare is usually about 5 extra minutes of thought. Not hours, not days. Five minutes to ask: will the next person understand this? Can they extend it? Will they have to undo what I did before they can make progress?
Sometimes those 5 minutes are actually 5 months in disguise, and sometimes it’s the other way around. Knowing which one you’re looking at is the actual skill, and I’ve gotten it wrong in both directions.
The EC2 instance outside CDK
One of my junior engineers manually provisioned an EC2 instance when we needed a server, fast, under a tight deadline. Clicking through the console is fast, I get it. The problem was that we ran everything through CDK, every piece of infrastructure defined as code. I came down on him pretty hard for it. The EC2 instance worked fine. The hidden cost was what it did to the system around it: when everything is defined as code, you know what exists, where it’s deployed, and how to reproduce it. When someone provisions something manually, you get a ghost. Six months later someone gets paged at 2am for a service being down, and nobody knows where it was deployed or how it was configured. CDK is also terrible at importing existing infrastructure, so migrating that manually created EC2 back into our managed stack took longer than if he’d just written the construct the first time.
That said, “everything as code” was non-negotiable at that scale, with that team size, at AWS. For my own startup products right now, I skip Terraform and create resources manually. A solo founder and a team of engineers who need to understand each other’s infrastructure are not solving the same problem. The principle isn’t “always do it the hard way.” It’s understand the cost of the shortcut before you take it.
Queries timing out at 30 seconds
On a recent consulting engagement I inherited a codebase where backend queries were timing out at 30+ seconds. The team had worked around it by hardcoding data into static JSON files, a reasonable call when you need to demo next week. When I wired the frontend to the actual API, the pages were unusable, even loading 100 entries was painfully slow. One giant JOIN across millions of rows was trying to do everything at once.
I’d already built ORM models for all their tables during my discovery phase, so instead of optimizing the raw SQL in place, I stripped the old queries out entirely and replaced them with ORM-based ones, decomposed into smaller, faster queries with pagination. Query time dropped from 30+ seconds to ~200ms.
| Before | After | |
|---|---|---|
| Backend query time | 30s | 0.2s |
Before is where the queries timed out. A raw-SQL version of the fix also landed near 200ms; the ORM version was 15% faster.
Out of curiosity I had Claude write the optimized version in raw SQL too, just to compare. The ORM-based query was 15% faster, probably because the ORM generates cleaner query plans for the patterns you’re actually using, while raw SQL scattered through a codebase tends to accumulate inefficiencies nobody notices. Both approaches would have gotten me to ~200ms, but replacing the queries with ORM-based ones meant every query in the codebase is now type-safe, composable, and maintainable. The next developer doesn’t have to parse raw SQL strings to understand what’s happening. I’m a big believer in ORMs, my favorite is EntGo, a graph-based ORM for Go I loved at Meta, but the principle holds regardless of language: represent your schemas as code, mirror the database, don’t scatter raw queries through your application.
The GraphQL server
Not every “do it right” instinct is correct, though. At a previous role, our engineering team decided to build a GraphQL server. It was the architecturally “right” choice: flexible queries, typed schema, great developer experience. We were excited about it, I was excited about it, and it was the wrong call. Building a GraphQL server properly took way too long, the complexity snowballed, and we delayed showing progress to stakeholders because nothing was “ready” yet, the exact mistake I write about in telling the story of your work. We should have built a REST API. It would have been simpler, faster to ship, and honestly better suited to what we needed. There’s also the security angle: GraphQL has a fundamental problem with infinitely nested queries, and exposing it to the world as a public API opens you up to query depth attacks that are genuinely hard to defend against. We only used it internally, so it was manageable, but I can’t recommend GraphQL for a public-facing API anymore. The edge cases are a nightmare. We thought we were spending the extra 5 minutes. Those 5 minutes were actually 5 months. KISS would have told us to ship the REST API and evaluate whether we actually needed GraphQL later. We didn’t.
The iframe and the missing user_id
On that same consulting engagement, the frontend was a Vue app iframing a separate Angular app through an Express server. I launched it locally and an entire section of the UI was blank. It had errored out, and that’s when I found the iframe. I could have wired the Vue app to the API around it and called it “working.” Instead I spent about 15 hours porting the Angular components into Vue using Claude Code and consolidating into one application. Two cross-dependent repos became one. Two build pipelines became one. The iframe that broke when it failed to load was gone. Fifteen hours was still the 5-minute choice, because the alternative was maintaining that complexity indefinitely.
Same engagement, same principle, different bug: users could upload documents, but their data wasn’t tagged with their user ID, so the RAG chatbot searched all data, not just theirs. I could have added a filter at the query layer (check user_id at read time, don’t bother tagging at write time) and it would have solved the immediate problem. Instead I added user_id at the source: a new documents table to track uploads, two new columns on the data table, and user_id threaded through the entire pipeline, from upload to extraction to storage to query. The extraction pipeline itself didn’t change. The database loader got two new columns. The query layer got one additional WHERE clause. Five extra minutes of schema design, but now usage analytics, per-user billing, and data export all have the foundation they need. Without it, every one of those features would have started with a data migration.
Five minutes or five months
The 5-minute rule doesn’t mean “always do it the right way.” I ask what the shortcut actually is, and try to be honest about it. Then I ask what it costs 6 months from now. If the answer is “someone gets paged and has no idea what this is,” spend the 5 minutes; if it’s “we’ll refactor this eventually and that’s fine,” maybe skip it. And I ask whether the “right way” is really 5 minutes, or secretly 5 months, because if you’re building a GraphQL server when a REST API would do, you’re not being thorough. You’re over-engineering.
Most of the difference between good code and bad code is whether someone paused for 5 minutes to think about the person who comes after them, and noticed when that pause had quietly turned into months of extra work. Some days that means writing the CDK construct. Some days it means shipping the REST API and moving on.