Andy Hinkle

Andy Hinkle

Laravel Developer


December 9, 2025

Does using the Circuit Breaker Pattern add too much noise?

You've got a simple third-party API call:

1class PaymentGateway
2{
3 public function charge(int $amount): PaymentResult
4 {
5 return Http::post('https://api.stripe.com/v1/charges', [
6 'amount' => $amount,
7 ])->json();
8 }
9}

Clean. Easy to read. Then one day the API starts failing intermittently. Your app hammers it with retries, users get timeout errors, and you decide to add a circuit breaker.

Now your code looks like this:

1class PaymentGateway
2{
3 public function __construct(
4 private CircuitBreaker $circuitBreaker
5 ) {}
6 
7 public function charge(int $amount): PaymentResult
8 {
9 if ($this->circuitBreaker->isOpen()) {
10 throw new ServiceUnavailableException('Payment service temporarily unavailable');
11 }
12 
13 try {
14 $result = Http::post('https://api.stripe.com/v1/charges', [
15 'amount' => $amount,
16 ])->json();
17 
18 $this->circuitBreaker->recordSuccess();
19 
20 return $result;
21 } catch (Exception $e) {
22 $this->circuitBreaker->recordFailure();
23 
24 if ($this->circuitBreaker->isOpen()) {
25 throw new ServiceUnavailableException('Payment service temporarily unavailable');
26 }
27 
28 throw $e;
29 }
30 }
31}

This is the complaint. What was 5 lines is now 25. The circuit breaker logic is tangled with your business logic. Every method that calls an external service needs this same boilerplate.

It doesn't have to be this way.

Let the Circuit Breaker Listen From Outside

Instead of weaving circuit breaker logic into your class, let it wrap your class from the outside. Your original code stays untouched.

Using a Decorator

Create a decorator that sits in front of your service:

1class CircuitBreakerPaymentGateway implements PaymentGatewayInterface
2{
3 public function __construct(
4 private PaymentGateway $gateway,
5 private CircuitBreaker $circuitBreaker
6 ) {}
7 
8 public function charge(int $amount): PaymentResult
9 {
10 return $this->circuitBreaker->call(
11 fn () => $this->gateway->charge($amount)
12 );
13 }
14}

Your original PaymentGateway class? Unchanged. Still that clean 5-line method. The circuit breaker wraps it from the outside, listening for failures without polluting the original code.

Bind it in your service container:

1$this->app->bind(PaymentGatewayInterface::class, function ($app) {
2 return new CircuitBreakerPaymentGateway(
3 new PaymentGateway(),
4 new CircuitBreaker('payments')
5 );
6});

Using a Base Class

If you have multiple services that need circuit breaking, use inheritance:

1abstract class ExternalService
2{
3 protected CircuitBreaker $circuitBreaker;
4 
5 protected function withCircuitBreaker(callable $operation): mixed
6 {
7 return $this->circuitBreaker->call($operation);
8 }
9}
1class PaymentGateway extends ExternalService
2{
3 public function charge(int $amount): PaymentResult
4 {
5 return $this->withCircuitBreaker(fn () =>
6 Http::post('https://api.stripe.com/v1/charges', [
7 'amount' => $amount,
8 ])->json()
9 );
10 }
11}

One extra line. The circuit breaker behavior is inherited, not implemented.

Using a Trait

Prefer composition over inheritance? Use a trait:

1trait HasCircuitBreaker
2{
3 protected function withCircuitBreaker(string $service, callable $operation): mixed
4 {
5 return app(CircuitBreakerManager::class)
6 ->for($service)
7 ->call($operation);
8 }
9}
1class PaymentGateway
2{
3 use HasCircuitBreaker;
4 
5 public function charge(int $amount): PaymentResult
6 {
7 return $this->withCircuitBreaker('payments', fn () =>
8 Http::post('https://api.stripe.com/v1/charges', [
9 'amount' => $amount,
10 ])->json()
11 );
12 }
13}

Same result. The trait brings in the capability without cluttering your class with implementation details.

The Pattern Isn't Noisy but Bad Implementation Is

The circuit breaker pattern itself doesn't add noise. Implementing it inline does. When you let the circuit breaker sit outside your code (as a decorator, base class, or trait) your business logic stays focused on what it actually does.

The circuit breaker listens in. It tracks failures. It opens and closes. But your code? Your code just makes the call.

That's the difference between a pattern adding noise and a pattern being implemented poorly. Use the tools PHP gives you to keep the resilience logic where it belongs: outside.

Cheers