5 min read
On this page

Knowing When You're Done

The hardest part of finishing work is knowing when to stop. Engineers are natural optimizers. Given unlimited time, we will refactor endlessly, add edge case handling for scenarios that will never occur, polish interfaces that nobody sees, and chase performance gains that do not matter. This is not diligence. It is scope creep on yourself. The discipline of shipping is knowing when work is good enough and moving on.

The Definition of Done

Every task needs a definition of done before you start. Not after. Not when you feel like it is good enough. Before you write the first line of code, answer: "What does done look like?"

Good definition of done:
  "The user can upload a CSV file, see a preview of the first
   10 rows, and confirm the import. Errors in the CSV are shown
   inline. A success message appears after import completes.
   Unit tests cover the parser and the validation logic. The
   PR is approved and merged."

Bad definition of done:
  "The import feature works."
  "It's done when I feel good about it."
  "When all the edge cases are handled."

The first definition is testable. You can objectively verify each statement. The second and third are feelings, and feelings never say "done."

Writing a Definition of Done

Template:
  1. What does the user see/experience when this is done?
  2. What tests exist to prove it works?
  3. What does NOT need to be done in this task?
  4. What is the acceptance criteria?

Example for "add password reset":
  1. User clicks "Forgot password," enters email, receives a reset
     link, clicks it, enters a new password, can log in with it
  2. Tests: reset email sent, link expires after 24 hours, used
     link cannot be reused, password meets strength requirements
  3. NOT in scope: rate limiting on reset requests (separate task),
     custom email template (using default for now)
  4. Acceptance: all tests pass, works on mobile, reviewed and merged

The "NOT in scope" section is the most important. It gives you explicit permission to stop.

Gold Plating

Gold plating is adding unrequested improvements that increase complexity without proportional value. It feels productive. It feels like craftsmanship. It is a trap.

Examples of gold plating:
  - Adding a caching layer "because it might be slow someday"
  - Supporting 5 file formats when the spec says CSV only
  - Building a plugin architecture for a feature with one implementation
  - Writing 400 lines of error handling for a prototype
  - Optimizing a function that runs once per day
  - Adding configuration options nobody asked for
  - Refactoring adjacent code that is not part of the task

The test for gold plating: does this directly serve the definition of done? If no, it is gold plating. Write it down as a future task if it is genuinely needed, and move on.

The YAGNI Principle

YAGNI — You Ain't Gonna Need It — is the antidote to gold plating. It says: do not build something until you actually need it. Not until you think you might need it. Until you actually need it.

"What if we need to support multiple databases?"
  -> You are using Postgres. You will be using Postgres next year.
     Do not abstract the database layer for a hypothetical migration.

"What if the API needs to handle 10x traffic?"
  -> It currently handles 10 requests per second. Optimize when
     you approach the limit, not when you imagine the limit.

"What if someone wants to customize this?"
  -> Nobody has asked. Build the simple version. If someone asks,
     add customization then. You will know what they actually need
     instead of guessing now.

Every abstraction, configuration option, and extension point you add now is code you must maintain forever. The cheapest code is code you did not write.

"Just One More Refactor"

The refactoring trap is subtle because refactoring is genuinely good practice. The problem is when refactoring becomes the task instead of serving the task.

Healthy refactoring:
  "I need to add a new payment method. The existing code is
   tightly coupled, so I'll refactor the payment interface to
   make my change clean. Then I'll add the new method."
  (Refactoring serves the task)

Unhealthy refactoring:
  "I was supposed to add a new payment method, but while I was
   reading the code I noticed the error handling is inconsistent.
   And the naming conventions are mixed. And this module could
   be split into two. Let me fix all of that first."
  (The task now serves the refactoring)

The rule: refactor only what you must touch to complete your current task. Note other improvements for later. Do not let the perfect codebase prevent you from shipping the feature.

The Boy Scout Rule, Applied Correctly

"Leave the code better than you found it" does not mean "rewrite everything adjacent to your change." It means:

Appropriate:
  - Fixing a typo in a variable name you are already editing
  - Extracting a function from a 200-line method you are modifying
  - Adding a missing null check in code you are calling

Not appropriate:
  - Rewriting the entire module you are touching
  - Changing the coding style of files you are not modifying
  - Migrating a dependency that is tangential to your task

Ship It, Then Iterate

The most productive engineers ship early and improve later. They know that the first version will be imperfect and that imperfect-but-shipped beats perfect-but-unshipped.

Shipping mindset:
  Version 1: It works. It has tests. It handles the common cases.
             Ship it.
  Version 2: Users report an edge case. Fix it. Ship it.
  Version 3: Usage grows. Performance matters now. Optimize. Ship it.
  Version 4: Requirements evolve. Refactor to accommodate. Ship it.

Non-shipping mindset:
  Version 1: It works, but the code could be cleaner.
  Version 1.1: Refactored, but now I want to add caching.
  Version 1.2: Cached, but the tests could be more comprehensive.
  Version 1.3: Better tests, but I noticed the error messages
               could be more helpful.
  Version 1.4: Still not shipped. It has been 3 weeks.

Version 1 of the shipping mindset delivers value on day 3. Version 1.4 of the non-shipping mindset delivers nothing on day 15.

The 80/20 of Quality

Most of the value in software quality comes from the first 80% of effort. The last 20% of quality improvement costs 80% of the total effort.

Quality investment curve:

  Effort      Quality          Value delivered
  20%         Basic tests      Catches obvious bugs (high value)
  40%         Edge cases       Catches likely failures (medium value)
  60%         Error handling   Graceful degradation (medium value)
  80%         Refactoring      Maintainability (some value)
  90%         Optimization     Performance (little value until needed)
  95%         Polish           Aesthetic code (almost no value)
  100%        Perfection       Does not exist

For most tasks, 60-80% effort is the right stopping point. Push to 90%+ only for critical paths: security, data integrity, core business logic, and high-traffic hot paths.

Practical Completion Checks

When you think you are done, run through this checklist:

Before submitting the PR:

  Functionality:
  [ ] Does it satisfy the acceptance criteria?
  [ ] Have I tested it manually for the common case?
  [ ] Do the automated tests pass?

  Quality:
  [ ] Is there anything I would be embarrassed by in code review?
  [ ] Did I handle the obvious error cases?
  [ ] Are there any TODOs I should address now vs later?

  Scope:
  [ ] Am I adding anything that was not in the original task?
  [ ] Am I refactoring beyond what my task requires?
  [ ] Would I ship this as-is if the deadline were today?

  If all checks pass: submit the PR.
  If the scope checks fail: revert the extras and submit.

The last question — "would I ship this as-is?" — is the most clarifying. If yes, you are done. If no, identify the specific thing that makes it unshippable and fix only that.

Real-World Example: The Feature That Was Done Three Times

An engineer was tasked with building a search feature. After 2 days, the basic search worked: users could type a query, results appeared, clicking a result navigated to it. Definition of done: met.

But the engineer thought:

  • "The results should be highlighted" (2 more hours)
  • "We should add fuzzy matching" (1 more day)
  • "The search should work across all entity types, not just the ones in the spec" (2 more days)
  • "The UI should show search suggestions as you type" (1 more day)

By the time the PR was submitted, the 2-day task had taken 7 days. The code review took 2 days because the PR was enormous. The fuzzy matching had a bug that caused another day of fixes. Total: 10 days for a 2-day task.

The team needed the basic search feature on day 3 for a demo. It was not available because it was still being gold-plated. Every addition after day 2 was a separate task that should have been scoped, estimated, and prioritized independently.

Common Pitfalls

  • No definition of done — without explicit completion criteria, you will never feel done. Define done before starting, including what is NOT in scope.
  • Gold plating as a habit — adding unrequested features feels like craftsmanship but is scope creep. If it is not in the acceptance criteria, it is a separate task.
  • Confusing "could be better" with "not done" — every piece of code could be better. That does not mean it is not done. Done means it meets the acceptance criteria and is shippable.
  • Refactoring instead of shipping — refactoring that serves your current task is good. Refactoring that replaces your current task is procrastination with extra steps.
  • Perfectionism as a virtue — perfectionism in software engineering is not a strength. It is a bottleneck. Ship good work, iterate based on feedback, and spend your perfectionism on the things that truly matter.

Key Takeaways

  • Define done before you start. Include what IS in scope, what IS NOT in scope, and testable acceptance criteria. This gives you explicit permission to stop.
  • Gold plating is adding unrequested improvements. Apply YAGNI: build what you need now, not what you might need someday. Write future improvements as separate tasks.
  • Refactor only what your current task requires. Note other improvements for later. Do not let the refactoring become the task.
  • Ship early and iterate. Version 1 that ships on day 3 is more valuable than version 1.4 that ships on day 15. Early feedback shapes better iteration than early speculation.
  • The 80/20 rule applies to quality. Most value comes from basic tests, error handling, and clean code. Push past 80% only for critical paths.