Why “Make It Work, Then Make It Right, Then Make It Fast” Produces Bad Systems at Scale
Kent Beck’s maxim is genuinely good advice for an individual working on a bounded problem. The argument here is that it does not scale — because at large teams and long time horizons, the three stages rarely happen sequentially, and the feedback loops that should trigger stages two and three are consistently broken.
Some pieces of advice in software engineering are so often repeated that they have crossed the line from principle into received wisdom — the kind of thing that gets nodded at in code reviews and cited in engineering blog posts without much examination of the assumptions they rest on. Kent Beck’s three-stage maxim — make it work, then make it right, then make it fast — is one of them.
The maxim is not wrong. In its original context it is, in fact, excellent advice. The problem is that the context in which it was formed — a single developer, a tight feedback loop, clear personal ownership of a bounded problem — bears almost no resemblance to the environment in which most professional software engineering actually happens. And when you transplant the maxim into large teams, long-lived systems, and distributed architectures, the implicit sequencing breaks down in ways that are predictable, consistent, and genuinely damaging.
1. The Maxim and What It Actually Assumes
To understand where the advice breaks down, it helps to start by making its hidden assumptions explicit. Beck’s formulation — popularised through Test-Driven Development: By Example and his broader XP writing — was developed in the context of test-driven development cycles, where the developer writes a failing test, makes it pass with the simplest possible code, then refactors. The three stages map directly onto the red-green-refactor loop.
That loop works beautifully because every assumption it makes is satisfied inside a TDD cycle:
Each of these assumptions is reasonable at the scale of a TDD cycle. Each of them, however, is systematically violated in the context of production systems, team rotation, and long time horizons. Furthermore, the violation is not accidental — it is a structural consequence of how production software actually evolves.
2. The Stage-One Trap: Working Code Enters Production
The most consequential break in the maxim’s logic happens at the transition from stage one to stage two. In a TDD cycle, the transition is immediate — you’ve made the test pass and now you refactor, while the code is still warm and the context is still in your head. In a production environment, stage one ends when the feature ships. And shipping changes everything.
Once working code enters production, it begins accumulating what economists would call switching costs: other services call it, data flows through it, users depend on its behaviour, other engineers write code that assumes its quirks. None of this is visible in the moment of shipping. But it accumulates rapidly, and it transforms the effort required to reach stage two from a matter of hours into a matter of weeks — or, in many cases, eliminates the realistic possibility entirely.
There is also a subtler problem that sits inside stage one itself. When an engineer writes the simplest code that makes something work, they are — quite deliberately — not writing tests for the edge cases, not handling the failure modes carefully, and not modelling the domain accurately. Under TDD, that is fine, because those things come in the refactor. However, under production conditions, those omissions become the foundation that subsequent engineers build on. The absence of tests, the rough domain model, and the unhandled failure modes all get treated as intentional decisions by anyone who comes to the code later.
“In a TDD cycle, messy working code is a temporary state. In a production system, messy working code is a load-bearing wall. The maxim does not distinguish between the two.”
3. Why the Feedback Loops That Should Trigger Stage Two Are Broken
In a TDD cycle, the signal to move from stage one to stage two is visceral and immediate: the test is green, the code is obviously ugly, and you refactor right now. That feedback loop is tight, personal, and unavoidable. At scale, every one of those properties is broken.
First, the signal is no longer clear. Production systems do not wave a flag when they are structurally wrong; they simply work, more slowly and more brittly over time. The feedback arrives as an increasing flow of bug reports, as escalating time estimates for new features, as unexplained incidents. By the time the signal is strong enough to act on, the system has been accumulating structural debt for years.
Second, the ownership is diffuse. The engineer who could most accurately judge whether stage two is needed is frequently not the person who will do it, not the person managing the team’s priorities, and not present when the relevant conversations are happening. Moreover, in many organisations, the engineer who wrote the stage-one code has left entirely. Their colleagues inherited the system without the context to evaluate whether its current shape is intentional design or deferred cleanup.
Studies of engineering team turnover suggest that the average tenure on a given product team is significantly shorter than the lifespan of the code that team produces. A system written in 2019 is almost certainly maintained today by people who were not present for any of its original design decisions — including the ones labelled “we’ll clean this up later.”
Third, and perhaps most critically, the safety net was deferred alongside the refactor. One of the most reliable patterns in production codebases is that test coverage is thin precisely in the areas that most need refactoring. This is not coincidence — it is causation. The decision to defer the refactor and the decision to defer the tests are usually the same decision, made at the same moment. As a result, when stage two finally becomes possible in principle, it is not safe in practice. Refactoring untested production code that other services depend on is a high-risk activity, and most engineering teams are correctly reluctant to take it on without significant dedicated time that never gets prioritised.
Likelihood of refactoring vs. time in production (composite industry data)

4. When Iterative Quality Is a Strategy vs. a Rationalisation
It would be unfair to suggest that the maxim is always misapplied. Iterative quality improvement is a genuine and valid strategy in the right conditions. The problem is that, in practice, “we’ll make it right later” is frequently used as a rationalisation for indefinite deferral rather than as a real plan with a real trigger and a real owner.
The distinction between a strategy and a rationalisation comes down to three questions. First, is there a concrete trigger — a measurable condition that will cause stage two to happen? Second, is there a named owner — a specific person or team who has capacity and authority to do it? Third, is there a realistic deadline — not a vague “after the launch” but a date on a roadmap?
Without all three, “make it right later” is not a plan. It is a way of making a decision feel less final than it is.
| Context | Iterative quality is… | Because… | Risk level |
|---|---|---|---|
| Solo developer, TDD cycle | Genuine strategy | Feedback loop is minutes, ownership is continuous, refactor is immediate | Low |
| Small team, pre-production prototype | Genuine strategy | No external dependencies yet, team has full context, cleanup is near-term | Low |
| Small team, post-launch feature | Conditional | Viable if explicit trigger and owner exist; high risk without them | Medium |
| Large team, multi-service system | Usually a rationalisation | Ownership diffuse, context decays fast, switching cost rises immediately | High |
| Long-lived system, team rotation | Almost always a rationalisation | Original context is gone, refactoring is unsafe without tests that were also deferred | Very High |
5. The Conditions That Break the Maxim at Scale
By this point the argument can be stated more precisely. Beck’s maxim breaks down not because it is wrong, but because it contains implicit conditions that cease to hold as systems grow. There are four of these conditions in particular, and each one deserves to be named explicitly.
5.1 Ownership continuity
The maxim assumes that the person who writes the working code is also around to make it right. In a TDD cycle, this is guaranteed. In a production system with any team turnover at all, it is not. Furthermore, the bus factor of stage-one code is frequently one — the engineer who knows what was deferred and why. When they leave, that knowledge leaves with them.
5.2 Feedback loop length
The maxim requires a feedback loop short enough that the working code is still plastic — still easy to reshape — when the signal to make it right arrives. In a TDD cycle, that loop is minutes. In a production system, the signal that something needs to be made right often arrives months or years after the code shipped, by which point it is deeply embedded and costly to change.
5.3 The absence of external dependents
At the moment of writing stage-one code in a TDD cycle, nothing else depends on it. In a production microservices environment, a service can acquire downstream consumers within days of deployment. Those consumers create a commitment — an implicit API contract — that makes structural changes expensive and risky even when everyone agrees they are necessary.
5.4 The co-deferral of tests and refactoring
This is the most important structural problem. When stage two is deferred, the tests that would make it safe to perform are almost always deferred at the same moment. As a result, the longer stage two is delayed, the harder it becomes — not just because the code has accumulated dependencies, but because the safety net required to refactor it safely was never built. This is not a failure of discipline; it is a predictable consequence of the incentive structure around delivery timelines.
# A pattern that appears in most long-lived production codebases:
# The test file mirrors the "we'll do this properly later" moment
# payment_processor.py — written during a tight sprint in 2021
# TODO: refactor this once the launch settles — @alice
def process_payment(amount, card_token, user_id):
# Direct DB write — should go through order service
# but that refactor was scoped for Q2 2021
result = db.execute("INSERT INTO payments ...")
# No retry logic — flagged in code review, deferred
# No idempotency key — known issue, tracked in JIRA-4471
return result
# test_payment_processor.py — written the same sprint
def test_process_payment_happy_path():
# TODO: add failure cases — @alice
# JIRA-4472: expand test coverage before refactor
assert process_payment(100, "tok_test", 42) is not None
# Alice left the company in August 2021.
# JIRA-4471 and JIRA-4472 were closed as "Won't Fix" in 2022.
# This code processed $2.4M in payments last month.
This example is stylised, but it is not a caricature. The pattern — a comment flagging a known problem, a JIRA ticket that gets quietly closed, a critical function tested only for its happy path, an author who is no longer reachable — appears in the production codebases of organisations of every size and maturity. It is the structural outcome of applying stage-one logic in a stage-two-never-arrives environment.
Relative cost of making it right: time of decision vs. time in production

6. A More Honest Account of Quality Deferral
None of this means that iterative delivery is wrong, or that teams should attempt to design systems perfectly before writing a line of production code. That approach carries its own serious risks — over-engineering, premature abstraction, and the inability to learn from real usage being the most significant. The argument is not against iteration; it is against the specific belief that quality work deferred is quality work that will eventually happen.
A more honest account of the tradeoff looks like this: when you defer stage two, you are not scheduling it for later — you are increasing the probability that it will never happen, in proportion to how load-bearing the stage-one code becomes and how quickly the team context around it decays. That is the actual bet being made, and it should be made with clear eyes.
Several practices shift this dynamic in a meaningful way. The most important is making the deferral explicit and bounded. Rather than a vague “we’ll refactor this later,” the decision should specify what trigger would cause the refactor to happen, who owns it, and what the cost of not doing it will be at that point. This is, in effect, applying the same logic as the expiry-triggered architectural test discussed in other contexts — turning a silent assumption into an active signal.
# A deferred quality decision made explicit and bounded # This is not a comment — it is a tracked, owned commitment ## DEFERRAL: Direct DB write in payment processor (2024-03-10) ## What was deferred: Routing through order service with idempotency keys ## Why deferred: Launch deadline; order service API not ready ## Owner: @payments-platform-team ## Trigger for stage two: order service v2 API available (est. Q3 2024) ## Cost if not done: Duplicate payment risk at >500 req/s (current: 120) ## Linked ticket: ENG-8841 (not optional, not closeable without resolution) ## Review date: 2024-09-01 — if not done, escalate to engineering director
Additionally, the decision about whether to defer should account for the co-deferral of tests. If a refactor is being deferred, the minimum responsible action is to write the tests that would make that refactor safe when it eventually happens — even if the refactor itself is not yet. This is a small additional investment at stage one that substantially increases the probability of stage two being achievable.
Finally, and perhaps most importantly, organisations should be honest about the contexts in which “make it right later” is simply not a realistic plan — where the system is already complex, the team rotation is high, and the dependencies are deep. In those contexts, the honest statement is not “we’ll make it right in Q3” but “we have accepted that this will remain structurally imperfect, and we have assessed that the cost of that is acceptable.” That is a legitimate engineering decision. Presenting it as a deferred improvement is not.
When stage two is genuinely being deferred rather than abandoned: document the specific trigger, name the owner, write the tests that make the eventual refactor safe, and create a non-closeable ticket with a review date. If none of these is possible, the honest description of the decision is “we are permanently accepting this quality level,” not “we will fix it later.”
7. What We Have Learned
- Beck’s maxim is genuinely valid within the TDD cycle it was designed for — it breaks down because of hidden assumptions about ownership continuity, feedback loop length, absence of external dependents, and safety net availability, none of which hold at scale.
- The transition from stage one to stage two is irreversibly complicated the moment working code enters production, because switching costs begin accumulating immediately and compound with every passing week.
- The feedback loops that should trigger stage two — clear signals, continuous ownership, short loops — are structurally broken in large teams and long-lived systems, not because of lack of discipline but because of how production environments actually evolve.
- Tests and refactoring are almost always deferred together, meaning that the longer stage two is delayed, the more unsafe it becomes — an inverse spiral that makes the cost of making it right grow faster than the motivation to do so.
- Iterative quality improvement is a genuine strategy when ownership is continuous, context is intact, and the feedback loop is short enough to keep code plastic. In any other context, it requires explicit triggers, named owners, and a realistic deadline to be meaningful.
- “Make it right later” without a concrete trigger, a named owner, and a bounded timeline is not a plan — it is a rationalisation for indefinite deferral, and should be recognised and recorded as such.
- The minimum responsible action when genuinely deferring stage two is to write the tests that would make the refactor safe when it eventually arrives, even if the refactor itself is not yet possible.
- In contexts of deep complexity, high team rotation, and dense dependencies, the honest engineering statement is “we accept this quality level permanently” rather than “we will fix it later” — and that decision should be documented with the same rigour as any other architectural choice.





