php 75 lines · 9 steps

How a circuit breaker guards a flaky service

A cache-backed circuit breaker that stops calling a failing dependency once errors pile up, then lets it recover.

Explained by highlit
1<?php
2 
3namespace App\Resilience;
4 
5use Psr\SimpleCache\CacheInterface;
6use RuntimeException;
7 
8final class CircuitBreaker
9{
10 private const CLOSED = 'closed';
11 private const OPEN = 'open';
12 private const HALF_OPEN = 'half_open';
13 
14 public function __construct(
15 private readonly CacheInterface $cache,
16 private readonly string $service,
17 private readonly int $failureThreshold = 5,
18 private readonly int $openDuration = 30,
19 ) {
20 }
21 
22 public function call(callable $operation): mixed
23 {
24 $state = $this->state();
25 
26 if ($state === self::OPEN) {
27 throw new CircuitOpenException("Circuit for '{$this->service}' is open");
28 }
29 
30 try {
31 $result = $operation();
32 $this->onSuccess();
33 return $result;
34 } catch (RuntimeException $e) {
35 $this->onFailure();
36 throw $e;
37 }
38 }
39 
40 private function state(): string
41 {
42 $openedAt = $this->cache->get($this->key('opened_at'));
43 
44 if ($openedAt === null) {
45 return self::CLOSED;
46 }
47 
48 if (time() - $openedAt >= $this->openDuration) {
49 return self::HALF_OPEN;
50 }
51 
52 return self::OPEN;
53 }
54 
55 private function onSuccess(): void
56 {
57 $this->cache->delete($this->key('failures'));
58 $this->cache->delete($this->key('opened_at'));
59 }
60 
61 private function onFailure(): void
62 {
63 $failures = (int) $this->cache->get($this->key('failures'), 0) + 1;
64 $this->cache->set($this->key('failures'), $failures, $this->openDuration * 2);
65 
66 if ($failures >= $this->failureThreshold) {
67 $this->cache->set($this->key('opened_at'), time(), $this->openDuration);
68 }
69 }
70 
71 private function key(string $suffix): string
72 {
73 return "circuit_breaker:{$this->service}:{$suffix}";
74 }
75}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1A circuit breaker fails fast once a dependency looks unhealthy, sparing callers from repeated slow errors.
  2. 2Storing state in a shared cache with TTLs lets the breaker expire and self-heal without a background job.
  3. 3Modeling three states — closed, open, half-open — gives you a controlled path back to normal operation.

Related explainers

Share this explainer

Here's the card — post it anywhere.

How a circuit breaker guards a flaky service — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code