DEV Community

Cover image for Best Practices Are a Myth. Trade-offs Are the Real Skill.
Gustavo Bispo
Gustavo Bispo

Posted on

Best Practices Are a Myth. Trade-offs Are the Real Skill.

A few years into my career, I sat in a design review where someone pushed back on a proposed service split with a single sentence: "that's just best practice." Nobody in the room argued with it. Not because it was right — looking back, I don't think it was, for the team we had at the time — but because arguing against something with the word "best" already in it feels like arguing that you enjoy writing bad code. So we shipped it. Eight months later, two engineers were spending most of their sprint fighting deployment ordering between services that had no real reason to be separate in the first place.

That's when it clicked for me: "best practice" isn't a fact about the world. It's a trade-off that won, once, somewhere, and then got repeated so many times it forgot it was ever a decision instead of a law. DRY. Microservices. Cache aggressively. Normalize your schema. Write tests first. Every one of these sounds like something you'd be foolish to argue with. And every one of them is quietly charging you something the person repeating it usually isn't naming.

The bill always shows up eventually

Take DRY. In the case it was actually coined for — logic that has to change in lockstep, like a tax calculation copy-pasted in three places — it's genuinely correct. But I've watched teams apply it to code that just looks similar today, extract a shared function, and feel good about the diff shrinking. A year and a half later that function has two callers pulling it in different directions every time either one changes, and nobody wants to touch it anymore because you can't predict what else breaks. Nothing got eliminated there. Duplication just turned into coupling, and coupling is the more expensive one to undo, because untangling a shared abstraction means understanding every caller at once, while duplicated code only ever asked you to understand one thing at a time.

Microservices are the same story with better marketing. Say the trade-off out loud and it's obvious: independent deploys and fault isolation, paid for with network calls where a function call used to be, distributed transactions, and an operations bill (service discovery, retries, tracing across boundaries) that a small team frequently can't carry. Call it "best practice" and it sounds like a milestone every serious company eventually hits. Call it a trade-off and it sounds like what it is — a great deal once you've actually hit the coordination limits of a monolith, and an expensive tax if you haven't.

Caching is the one where the cost is honestly right there in the name of the problem it causes — cache invalidation. Every cache is a bet that stale data is cheaper than recomputing it. Fine bet on a product listing page. Much worse bet on an account balance. "Cache aggressively" has no opinion on which situation you're actually in.

And normalization — this one gets taught as pure correctness, no duplicated data, one source of truth, nothing can drift. That's real, and ignoring it does rot into data nobody trusts. But it's still a trade-off: every extra table is a join, and joins get slower as tables grow, until a dashboard that used to load instantly takes four seconds under real traffic. Third normal form doesn't know or care whether you're building a system of record that can never drift, or a reporting view where a deliberately denormalized copy isn't a shortcut, it's just the right call.

Even test-first isn't a virtue on its own — it's a trade. You pay design time up front, because writing the interface before the implementation forces you to think about the shape of the problem before you've solved it, in exchange for a safety net on refactors you haven't done yet. Great trade for code that'll live for years and pass through a dozen hands. Weaker trade for a script you're deleting next sprint, where you pay the cost and never collect the benefit.

Why nobody wants to say "it depends"

Best-practice lists stick around because they're cheap to say and expensive to argue with. "We follow best practices" ends a conversation. "Here's what we're optimizing for, and here's what we're giving up" starts one — and starting that one is uncomfortable, because the honest answer is usually "it depends," and "it depends" doesn't fit on a slide.

There's a sneakier reason too: the timing is asymmetric. The benefit of a decision shows up immediately — the diff looks cleaner, the demo works, the review passes without friction. The cost shows up later, to someone else, in a context that doesn't obviously trace back to the original choice. From inside that gap, it's really easy to mistake "I haven't personally paid for this yet" for "this was free." By the time the bill arrives, whoever made the call has usually moved teams, and the same practice gets repeated somewhere new, unexamined, again.

What I actually ask now, instead of checking a list

I stopped trying to remember which practices are "correct" and started asking three questions before adopting any of them.

What am I actually trading? Not "is this good," but what does it buy me, what does it cost, and in what currency — latency for consistency, flexibility for simplicity, this sprint's velocity for next year's ability to change direction. If I can't finish that sentence, I haven't understood the decision. I've just recognized the pattern and reached for its label.

Who pays for it, and when? A choice that saves this sprint and costs the team eight months from now isn't automatically wrong — sometimes shipping now really is the right call — but it should be said out loud, not quietly discovered later by whoever inherits it with no idea it was a deliberate trade instead of an oversight.

And, the one nobody ever asks: what would have to be true for this to stop being worth it? Checklists don't expire, so they never make you ask this. A cache is worth it until staleness causes a real incident. A shared function is worth it until its third caller needs different behavior. Naming the expiration condition ahead of time is the only reason anyone ever actually notices when "we'll revisit this later" arrives, instead of that sentence just quietly being a lie the team tells itself.

None of this is faster than "just follow the practice." It's not supposed to be. The speed of a checklist is exactly the problem — it lets a team ship a decision that nobody had to justify, which means nobody's watching for the moment the context that justified it quietly stops being true.

A PR comment, two ways

You can see this play out in something as small as a pull request. Somebody submits a change that duplicates about fifteen lines of validation across two handlers. One reviewer writes: "extract this, per DRY." That ends the discussion — it's a rule, and nobody wants to be the person arguing against a rule.

A different reviewer writes: "these two have validated the same shape and changed together every time the schema's changed — I'd pull this into a shared validator." That's not a rule, it's a claim, and it's checkable. Somebody else can reply, "actually the mobile handler split off twice last quarter over platform-only fields, I'd leave them separate and eat the duplication." Now you're having an actual conversation about the code's real history, instead of whoever cited the more prestigious acronym winning by default.

Multiply that difference by every small decision a team makes over a year, and that's most of the gap between a team that's busy and a team that's actually building something on purpose.

The skill underneath all of this

The engineers I respect most for this aren't the ones with the longest list of practices memorized. They're the ones who can walk into an unfamiliar codebase, look at a weird decision, and reconstruct what trade-off it was probably making when someone made it — then check whether that trade-off still holds today. You don't get that from a blog post (including this one). You get it from having personally paid for enough of these trades — the abstraction extracted too early, the cache nobody remembered to invalidate — that you start recognizing the shape of it before it fully plays out again.

Best practices aren't wrong. They're trade-offs with the context sanded off, and most of them are decent starting guesses, earned by someone who hit the same wall before you did. The mistake isn't using them. It's treating them as something you don't have to check.


Gustavo Bispo is a Senior Software Engineer working across frontend, backend, and architecture on distributed, remote engineering teams.

Top comments (0)