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
ruby
class Order class InvalidTransition < StandardError; end TRANSITIONS = {
A state machine for order transitions in Ruby
state-machine
data-driven
error-handling
Intermediate
8 steps
php
<?php namespace App\Http\Controllers;
Streaming a filtered CSV export in Laravel
streaming
csv-export
query-builder
Intermediate
9 steps
python
import time import threading from enum import Enum from functools import wraps
Building a circuit breaker in Python
circuit-breaker
resilience
decorators
Advanced
7 steps
php
<?php namespace App\Http\Middleware;
Idempotent requests with Laravel middleware
idempotency
middleware
caching
Advanced
8 steps
python
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector from django.core.cache import cache from django.db.models import F from django.http import JsonResponse
Ranked full-text search in Django
full-text-search
debouncing
caching
Advanced
9 steps
php
<?php namespace App\Services;
Batch image resizing with PHP GD
image-processing
constructor-promotion
match-expression
Intermediate
10 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.