The Red Green Refactor Cycle Is Simpler Than Most TDD Articles Make It Look

by Eric Hanson, Backend Developer at Clean Systems Consulting

The Mechanics Without the Philosophy

Red-green-refactor is a tight feedback loop, not a methodology you adopt. Each iteration takes between ninety seconds and five minutes. You run the tests constantly. The color of the test output is a signal: red means there is work to do, green means the behavior is correct, and refactor means you can now improve the code without changing its behavior.

That is the entire cycle. The philosophy is interesting but optional for getting started. The mechanics are what matter.

Red: Write One Failing Test

Write a single test for the next behavior you want the code to have. Run the test. It fails — either with a compilation error (the class or method does not exist yet) or with an assertion failure (the method exists but returns the wrong thing).

The test should be specific enough to be falsified: not "it returns something" but "it returns 85.0 given a price of 100 and a discount rate of 0.15."

The most common mistake in this phase: writing too much test at once. One behavior, one test. If you find yourself writing a test with five assertions covering three scenarios, stop and pick the simplest one.

# Red: Write this, run it, watch it fail
def test_no_discount_applied_when_rate_is_zero():
    calculator = PriceCalculator()
    result = calculator.apply_discount(price=100.0, rate=0.0)
    assert result == 100.0
# NameError: name 'PriceCalculator' is not defined
# Good. Now make it pass.

Green: Write the Minimum Code to Pass

Not the correct code, not the complete code — the minimum code that makes this specific test pass. This constraint is intentional and important.

If the test expects apply_discount(100.0, 0.0) to return 100.0, the minimum implementation is:

class PriceCalculator:
    def apply_discount(self, price: float, rate: float) -> float:
        return 100.0  # Hardcoded. Ugly. Correct for now.

This is not the final implementation. It is a temporary fake that passes the current test. When you write the next test — apply_discount(200.0, 0.0) returns 200.0, for instance — this fake will fail, forcing you to generalize.

Developers resist this step because it feels like writing bad code deliberately. The point is that you should only generalize the implementation when a test forces you to. This prevents over-engineering: you never build complexity the current tests do not require.

# Next test forces generalization
def test_no_discount_when_rate_is_zero_any_price():
    calculator = PriceCalculator()
    assert calculator.apply_discount(price=200.0, rate=0.0) == 200.0
    assert calculator.apply_discount(price=50.0, rate=0.0) == 50.0

# Now minimum implementation must be the real formula
class PriceCalculator:
    def apply_discount(self, price: float, rate: float) -> float:
        return price * (1 - rate)

Refactor: Improve Without Changing Behavior

The test is green. Now you can clean up: rename variables, extract helper methods, remove duplication, simplify conditionals. Whatever improves readability or structure without changing what the code does.

Run the tests after every change. If they go red, you changed behavior — undo the last change. The tests are your safety net during refactoring, which is why TDD-produced code is relatively easy to improve over time.

Refactoring is often skipped under time pressure. This is the step where the design quality that TDD advocates promise actually gets built. Skipping it means you get tested code but not cleaner code.

The Cycle in a Full Example

# Iteration 1: Red
def test_full_price_when_no_discount():
    assert PriceCalculator().apply_discount(100.0, 0.0) == 100.0

# Green (fake): return 100.0

# Iteration 2: Red (forces generalization)
def test_discount_reduces_price():
    assert PriceCalculator().apply_discount(100.0, 0.1) == 90.0

# Green (real formula): return price * (1 - rate)

# Iteration 3: Red (boundary case)
def test_full_discount_zeroes_price():
    assert PriceCalculator().apply_discount(100.0, 1.0) == 0.0

# Green: existing formula handles this

# Iteration 4: Red (invalid input)
def test_negative_discount_rate_raises():
    with pytest.raises(ValueError):
        PriceCalculator().apply_discount(100.0, -0.1)

# Green: add validation

# Refactor: add type hints, rename for clarity, extract validation

Five iterations, roughly ten minutes, and you have a fully specified, tested implementation that handles the happy path and the failure cases. The tests drove you to think about the negative rate case, which you might have missed implementation-first.

The cycle is that simple. The skill is keeping each iteration small enough that you always know exactly what you are building next.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Dubai Has No Local Backend Talent Pipeline — Every Hire Is a Global Search

You posted a backend role in Dubai. Half the applicants are in India. A quarter are in Europe. The ones already in the UAE want AED 40K per month. Nobody is local and cheap.

Read more

Database Indexing in Rails — What I Check Before Every Deploy

Missing indexes are the most common cause of avoidable database performance problems in Rails applications. Here is the pre-deploy checklist I run and the index decisions that actually matter.

Read more

How to Give Code Feedback Without Making It Personal

Code review feedback that feels like criticism of the person rather than the code creates defensiveness, damages collaboration, and produces worse outcomes than no feedback at all. The mechanics of giving it well are learnable.

Read more

Clear Acceptance Criteria in Backend Development

Clear acceptance criteria define exactly when a backend deliverable is considered complete. By setting measurable standards for performance, testing, and reliability, both the client and developer can verify the result with objective benchmarks.

Read more