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

by Arif Ikhsanudin, Backend Developer

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

How I Give Technical Feedback Without Killing Morale

Technical feedback is necessary, occasionally uncomfortable, and often delivered in ways that make people defensive instead of thoughtful. Here's the approach I've landed on after getting it wrong enough times.

Read more

Being Good at the Work Is Not Enough. You Have to Be Easy to Work With.

Technical skill gets you considered. Everything else determines whether you get hired, kept, and referred. Most contractors underinvest in the everything else.

Read more

Designing APIs That Last — Principles From 10 Years of Breaking Things

An API is a contract. Breaking it breaks your users. The design decisions that seem minor at launch — naming, error shapes, pagination, versioning — are the ones that cost the most to change later. Here is what holds up and what doesn't.

Read more

Citadel and CME Group Pay Chicago's Backend Developers More Than Most Startups Can Afford

Chicago has world-class backend engineering talent. The financial firms that employ most of it have built compensation structures specifically designed to keep it.

Read more