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 to Design a System That Recovers Gracefully Without Human Intervention

Systems that require a human to notice and act on every failure do not scale operationally. Designing for automated recovery means defining recovery actions explicitly in the design, not during the incident.

Read more

The Hidden Costs of Hiring a Full-Time Backend Engineer Nobody Talks About

The salary is the number everyone negotiates. It's not the number that surprises founders six months into a backend hire.

Read more

Why Productivity Surveillance Harms Remote Developers

Watching every keystroke doesn’t make work faster. It often makes developers anxious, distracted, and less productive.

Read more

The Difference Between a Mock, a Stub, and a Fake That Actually Matters

Mock, stub, and fake are used interchangeably in most codebases, but they describe different things with different tradeoffs. Knowing which to reach for — and when — determines whether your test doubles make tests clearer or more confusing.

Read more