Implement a circuit breaker decorator
Build a production-grade circuit breaker as a Python decorator. It should track consecutive failures, open the circuit (raising an error immediately without calling the wrapped function) after 3 failures, transition to half-open after 30 seconds, and close again when the next call succeeds.
Why this matters
Circuit breakers prevent a slow or failing downstream service from cascading into a full system outage. Without them, threads pile up waiting for a timeout, memory exhausts, and a single dependency brings down an entire API. Every experienced backend engineer has been paged for exactly this at least once.
Before you start
- Comfortable writing Python decorators (the @functools.wraps pattern)
- Understanding of what an exception is and how to catch and re-raise
- Familiarity with time.time() or datetime for measuring elapsed time
- Basic understanding of why microservices need failure isolation
Step-by-step guide
- 1
Define the state machine
The circuit breaker has three states: CLOSED (normal operation), OPEN (failing fast), and HALF_OPEN (testing recovery). Draw the state transitions on paper before writing code: failure threshold in CLOSED -> OPEN; timeout elapsed in OPEN -> HALF_OPEN; success in HALF_OPEN -> CLOSED; failure in HALF_OPEN -> OPEN.
- 2
Write the CircuitBreaker class
Create a class that holds state, failure count, and the time the circuit opened. Implement a call() method that checks state and either executes the function or raises CircuitOpenError immediately. Keep the class separate from the decorator; this makes it testable and reusable.
- 3
Wrap it as a decorator
Write the @circuit_breaker decorator that creates a CircuitBreaker instance (or accepts one as an argument) and wraps the target function. Use functools.wraps to preserve the wrapped function's name and docstring. Test that the decorator can be applied to both sync and async functions.
- 4
Test all three state transitions
Write four tests: one for normal operation, one that triggers the open state after 3 failures, one that verifies calls fail fast while open, and one that verifies recovery via the half-open state. Mock time.time() to test the 30-second timeout without actually waiting.
- 5
Add a fallback parameter
Allow the decorator to accept an optional fallback function that is called when the circuit is open instead of raising an error. This is how production circuit breakers work; they return stale cache or a degraded response rather than a hard error.