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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A circuit breaker fails fast once a dependency looks unhealthy, sparing callers from repeated slow errors.
- 2Storing state in a shared cache with TTLs lets the breaker expire and self-heal without a background job.
- 3Modeling three states — closed, open, half-open — gives you a controlled path back to normal operation.
Related explainers
php
<?php class NameParser {
Parsing a full name into components in PHP
string-parsing
arrays
normalization
Intermediate
8 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
python
import secrets from django.contrib.auth import authenticate, login from django.core.cache import cache
Two-factor login with OTP in Django
two-factor-auth
one-time-passwords
caching
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-a-circuit-breaker-guards-a-flaky-service-explained-php-a34a/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.