← Back to Blog

Good Enough Is a Strategy

In engineering, perfectionism is often procrastination disguised as craftsmanship. Shipping an 80% solution and iterating beats a perfect solution shipped late.

I spent six weeks building the most elegant API I had ever designed. Every endpoint was perfectly named. The error codes were exhaustive and consistent. The pagination was cursor-based, the versioning was in the headers, the rate limiting was granular per-resource. I had written forty pages of internal documentation. I had reviewed every RFC that touched HTTP semantics. I was proud of it in the way you can only be proud of something you've poured way too much of yourself into.

Then we shipped it. And within three months, half of it had been ripped out because the product direction changed, and the other half was being quietly ignored by the mobile team who had just started passing everything as query parameters because it was easier for them. The forty pages of documentation became the thing nobody reads but also nobody deletes because it feels wrong to.

I did not build a great API. I built an expensive monument to my own standards. And I learned something I should have learned earlier: perfectionism in engineering is not a virtue. It is, most of the time, procrastination disguised as craftsmanship.

The Seduction of Perfect

The instinct toward perfection is not irrational. We have been trained to care about quality. Code reviews, design docs, linting rules, architecture diagrams. All of it reinforces the idea that the right answer exists and you should find it before you ship. In some domains, this is correct. In most, it is the wrong frame entirely.

The problem is that perfection optimizes for the wrong thing. It optimizes for the solution being correct in isolation, evaluated against abstract standards. What it does not optimize for is the solution being correct for the problem as it actually exists, in the time available, given what you will learn by shipping. Production is the only environment that tells you the truth. Every day you spend not shipping is a day you are reasoning from incomplete information.

There is a version of this that engineers find uncomfortable: the version you ship will teach you more than the version you plan. Not because planning is bad. Planning is necessary. But because planning has diminishing returns. The first hour of thinking through a design is enormously valuable. The fourth day is mostly anxiety and bikeshedding dressed up as rigor.

Where Perfectionism Costs You

Perfectionism is not equally harmful everywhere. It concentrates damage in a few specific patterns that most engineering teams encounter repeatedly.

Over-Engineered APIs

I already told you my story. Here is the pattern I see underneath it: engineers design APIs for a future user they have imagined, not the actual user in front of them. Imagined users care deeply about REST purity. The actual user is a mobile developer on a deadline who wants to know what parameters to pass and what to do when something fails.

A perfect API takes three times as long to build as the good-enough one. It covers edge cases nobody has hit yet. Its extensibility points point in directions the product has not gone and may never go. And it is versioned from day one, which means you now have two APIs to maintain before the product has found product-market fit.

Ship the API that handles the cases you have. Iterate when you hit the next one. Most endpoints will be called by three clients internally and never need to be changed.

Premature Optimization

The quote is so old it has become noise, but it is still true: premature optimization is the root of a lot of expensive distractions. Engineers spend days making a query twenty percent faster when the query runs once per hour. They add caching to a feature that has twelve users. They refactor a hot path that is not hot yet, has never been profiled, and may never matter because the feature might be cut in Q2.

Optimization is valuable when you know what is slow, why it is slow, and how much it matters. Before those three things are true, optimization is speculation executed at engineering cost. It is a guess wearing the clothes of craftsmanship.

The discipline here is not to stop caring about performance. It is to care about it at the right time: after you have users, after you have measured, and after you understand the actual shape of the bottleneck. Before that, your job is to ship something that works.

Test Coverage Theater

A ninety-five percent code coverage number feels like safety. It is not. Coverage measures which lines of code were executed during tests. It does not measure whether the tests are testing the right things, whether the assertions are meaningful, or whether the tests would catch an actual regression.

I have seen test suites with ninety percent coverage that missed every important bug because the tests were written to get coverage, not to find problems. I have seen test suites with sixty percent coverage that caught every regression that mattered because the engineers thought hard about what could actually go wrong and tested those paths specifically.

Writing tests until you hit a coverage number is theater. It produces confidence that is not warranted. The right amount of testing is enough to tell you when something you care about breaks. For most features, that is substantially less than a hundred percent coverage of every branch.

Write tests for the things that matter: the happy path, the edge cases that have actually caused problems, the behavior that other parts of the system depend on. Skip the tests that only exist to make the number go up.

Architecture Astronautics

The pattern: a simple problem gets solved with a sophisticated architecture because the engineer anticipated future scale, future complexity, or future requirements that may never arrive. The result is a system that is harder to understand, harder to change, and harder to debug than the problem actually warranted.

I have seen a CRUD feature implemented with event sourcing because the team thought they might want an audit trail someday. The audit trail requirement never materialized. The event sourcing never went away. Every new engineer who joined the team spent two weeks understanding why a simple read-your-writes feature needed a projection rebuild step.

The monolith that works fine is not a failure. The microservice that was extracted before the team had any real load is not a success story. Architecture should match the actual problem, not the imagined future problem. When the future problem arrives, you will have more information and more resources to address it than you do today. Solving it today means solving it with incomplete information and no feedback from production.

Where Good Enough Is Not Enough

I want to be careful here because I am not arguing for sloppiness. Good enough is a strategy, not an excuse. There are domains where the cost of getting it wrong is high enough that perfectionism is the right call. And engineers need to be honest about which domains those are, because the temptation to apply the same rigor uniformly in all directions is also a trap.

Security

There is no MVP for authentication. There is no iteration on encryption. You do not ship a payment flow with a known XSS vulnerability because you ran out of time and plan to fix it later. Security failures have a different cost profile than other failures: they are difficult to detect, difficult to recover from, and the harm extends to your users, not just to your business.

The same engineer who should move fast and iterate on a UI feature should stop and think carefully about every piece of code that handles credentials, sessions, authorization, or data that belongs to other people. These are not the place to prototype.

Data Integrity

A bug in a UI is usually a bug in a UI. A bug in a write path can be a bug in your data, which means it is a bug in every system that reads that data, now and in the future. Data corruption is expensive to detect, expensive to remediate, and sometimes impossible to fully recover from.

Write constraints. Write validation at the database level, not just the application level. Think about what happens when the application is wrong, because it will be wrong. The data layer is the last line of defense, and it needs to be designed as if the application will fail, because it will.

Financial Transactions

Money math needs to be exact. Rounding errors that are invisible in a demo are unacceptable in a billing system. Idempotency that you can skip in most contexts is non-negotiable when the alternative is charging someone twice. The retry logic that you can punt on for a notification service is critical when the operation is an account debit.

Financial systems are also where the cost of a bug is the clearest: it shows up in someone's bank account. That legibility makes the case for rigor easy. Apply it accordingly.

Public API Contracts

The API you expose internally you can change when you need to. The API you expose publicly, you own forever. Every field you add is a promise. Every field you remove is a breaking change. Every behavior that developers have relied on, even behavior you did not intend, becomes something you need to deprecate carefully and with notice.

Public API design is worth the extra time. Not because the implementation needs to be perfect, but because the interface is load-bearing in a way that internal interfaces are not. Get the surface right. The implementation can evolve.

A Framework for Deciding

When I am trying to decide how much is enough, I use three questions. They are not complicated.

What is the cost of being wrong? If the answer is "we fix it in the next sprint," the bar can be lower. If the answer is "we corrupt user data" or "we have a security breach," the bar needs to be higher. The cost of failure should determine the level of care, not abstract standards of engineering quality.

How hard is it to fix later? Some decisions are easy to change. You can refactor a poorly-named function in an afternoon. You cannot easily migrate a production database schema that has been in place for two years. The harder something is to change later, the more carefully you should think about it now. The easier it is to change, the faster you should move.

Who is waiting? This is the question engineers are most likely to dismiss and most likely to regret dismissing. Somebody is waiting for this feature. Maybe it is a customer who has been asking for three months. Maybe it is a product manager who is trying to close a deal. Maybe it is a user who currently cannot do something they need to do. The cost of your perfectionism is not just time on a roadmap. It is a real person waiting for a real thing. Weight that.

What Deferring 20% Actually Means

Shipping 80% is not the same as shipping 80% and hoping nobody notices the rest. It means explicitly deciding what you are deferring and when it becomes necessary to address it.

The 20% you are deferring should be written down somewhere. Not in your head. Not in a comment in the code that says // TODO: handle this case. In a ticket, with a description of what the missing piece is and what the trigger is for when it needs to be done. Maybe the trigger is "when we hit 10,000 users." Maybe it is "when the mobile team asks for this endpoint." Maybe it is "if we see more than 5 support tickets about this edge case in a month."

The discipline is in the deferral being deliberate and documented, not accidental and invisible. Technical debt that is tracked is manageable. Technical debt that is invisible compounds silently until it becomes a crisis.

The other thing shipping 80% means: you have to actually go back for the 20% when the trigger is hit. Not someday. Not eventually. When the condition you specified is true. This requires honesty about what the triggers are and discipline about checking them. Without this, "shipping 80%" is just "shipping 80% and lying to yourself."

The Version You Ship Teaches You

Here is the thing I keep coming back to. The plan is a hypothesis. It is your best guess about what the right solution is, made with incomplete information, in a room that does not contain any of your users.

Production is not a room. It is the actual world. And the actual world has a way of disconfirming hypotheses that felt airtight in the planning meeting. The API that made perfect sense to you will be used in ways you did not anticipate. That feature that took six weeks to build will be ignored by eighty percent of your users and intensely used by the other twenty in one specific way you did not design for. Your performance bottleneck will sit in a place you did not optimize, because you were busy optimizing somewhere else.

None of this is a failure. It is information. But you can only get it by shipping. The version you plan does not generate information. Iterating on a version in design reviews generates arguments. Only the version you put in front of users generates data. And data is the only thing that makes the next decision better than the last one.

Perfectionism is the belief that you can get to the right answer without the data. You cannot. Ship the thing. Learn. Make it better. That sequence is the actual job.

The Discipline of Good Enough

I want to end with this: good enough is not easy. Knowing when something is ready, and being honest that it is ready when it does not feel ready, is harder than continuing to polish. The feeling that something could be better is always true. Everything could always be better. The question is whether making it better is the highest-value use of the time available.

That question takes judgment. Judgment is harder than craftsmanship, because craftsmanship has clear outputs and judgment does not. You can show someone a well-written function. You cannot easily show someone a well-timed decision to stop improving something and ship it.

But that judgment is the job. It is what separates engineers who build things from engineers who design things. And in my experience, the discipline of knowing when to stop is the thing that separates the engineers who ship the most value from the ones who build the most impressive things that nobody ended up using.

Ship the 80%. Track the 20%. Come back for it when the data tells you to. That is not cutting corners. That is engineering.

Share
X LinkedIn HN
UI

Umur Inan

Principal Software Engineer

Backend engineer focused on JVM systems, distributed architecture, and the failure modes that only show up in production. I write about what I learn building and breaking things at scale.

👁 0 10 min read

Comments (0)