A circuit breaker is not a retry loop with a fancier name. Its job is to stop sending work to a dependency that is already failing, wait for a bounded recovery window, then admit a carefully controlled probe. In a backend or QA interview, being able to name and test those transitions is far more persuasive than drawing the pattern from memory.
What should a circuit breaker protect?
Suppose a checkout API calls a recommendation service. If that service starts timing out, every new request can consume a connection, a worker slot, and the caller's timeout budget. Retrying immediately may amplify the failure. A circuit breaker deliberately fails fast while the dependency is unhealthy.
The useful mental model has three states:
| State | What callers experience | What changes it |
|---|---|---|
| Closed | Requests reach the dependency. | Enough consecutive failures open it. |
| Open | Calls fail immediately; the dependency gets breathing room. | The cooldown expires. |
| Half-open | A limited recovery probe is allowed. | A successful probe closes it; a failed probe opens it again. |
That last row is the interview trap. “Wait a minute and retry” is incomplete. Without a half-open rule, every waiting caller can stampede the recovering dependency at once.
Build the state machine before choosing a library
Here is a small, dependency-free implementation. It is intentionally not production middleware. It is a compact model you can execute, inspect, and use to explain the contract.
import assert from 'node:assert/strict';
function createCircuitBreaker({
threshold = 3,
resetAfterMs = 1_000,
now = () => Date.now(),
} = {}) {
let state = 'closed';
let failures = 0;
let openedAt = 0;
let probeInFlight = false;
async function execute(operation) {
if (state === 'open') {
if (now() - openedAt < resetAfterMs) {
throw new Error('circuit open');
}
state = 'half-open';
}
if (state === 'half-open') {
if (probeInFlight) throw new Error('probe already running');
probeInFlight = true;
}
try {
const result = await operation();
failures = 0;
state = 'closed';
return result;
} catch (error) {
failures += 1;
if (state === 'half-open' || failures >= threshold) {
state = 'open';
openedAt = now();
}
throw error;
} finally {
probeInFlight = false;
}
}
return { execute, stateOf: () => state };
}
There are two deliberate design choices here.
First, the clock is injected. A test that waits one real second is slow and flaky; a test that advances a fake clock is deterministic. Second, only one half-open probe can run. That is a local-process guard, not a distributed one, but it makes the policy visible.
Test transitions, not implementation details
A test suite should prove the externally useful rules: three failures open the circuit, a call during the cooldown is rejected without reaching the dependency, and a healthy probe restores service.
let time = 0;
const breaker = createCircuitBreaker({ now: () => time });
const failure = () => Promise.reject(new Error('downstream 503'));
await assert.rejects(() => breaker.execute(failure), /503/);
await assert.rejects(() => breaker.execute(failure), /503/);
await assert.rejects(() => breaker.execute(failure), /503/);
assert.equal(breaker.stateOf(), 'open');
time = 999;
await assert.rejects(
() => breaker.execute(async () => 'should not run'),
/circuit open/,
);
time = 1_000;
assert.equal(await breaker.execute(async () => 'healthy'), 'healthy');
assert.equal(breaker.stateOf(), 'closed');
console.log('circuit-breaker assertions passed');
Run the combined snippets with Node 18+:
node circuit-breaker.mjs
The expected output is circuit-breaker assertions passed. For an extra drill, make the half-open probe fail and assert that the breaker returns to open with a fresh cooldown. That catches a surprisingly common bug: treating a recovery probe like an ordinary failure and accidentally allowing traffic through.
What changes in a real service?
The code above keeps state in one Node.js process. Production services have a few more questions that are worth saying out loud in an interview:
- Is failure counted per dependency, endpoint, tenant, or request class? A global breaker can make one noisy route affect unrelated work.
- Which failures count? Timeouts and connection errors often do; a caller's invalid
400response usually should not. - How do multiple instances coordinate? A local breaker contains damage per instance. A shared breaker needs a store and careful availability trade-offs.
- What is the fallback? A cached response, a queued request, or a clear degraded response can all be valid. Silence is not a fallback.
- Which metrics prove it works? Track state transitions, rejected calls, probe outcomes, and downstream latency separately.
The AWS Circuit Breaker pattern and Martin Fowler's Circuit Breaker describe the same core goal from operational and application-design angles: prevent a known bad dependency from consuming the rest of the system.
How to explain this in an interview
A concise answer sounds like this:
“I would open the circuit after a bounded run of dependency failures, fail fast during a cooldown, and allow one half-open probe. I would test each state transition with an injected clock, then define failure classes and metrics before deciding whether coordination must be shared across instances.”
That answer gives an interviewer a place to probe. If they ask about retries, say retries belong inside a bounded budget and should not bypass an open circuit. If they ask about scale, distinguish local protection from globally coordinated policy.
For mock practice, aceround.app — an AI interview assistant can turn those follow-up questions into a timed verbal drill. The important part is still your own explanation of the state transitions and trade-offs.
Sources and disclosure
- Microsoft Learn, “Circuit Breaker pattern,” accessed July 2026.
- Martin Fowler, “CircuitBreaker,” accessed July 2026.
Disclosure: AI assisted with drafting and editing. The code, state transitions, source links, and test behavior were reviewed before publication.
Top comments (0)