Skip to Content
Course content

196: Practice Exercise: Implementing a Circuit Breaker from Scratch

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you're building a distributed system, you have to assume that every single external dependency—whether it's a legacy SOAP service or a modern REST API—will eventually fail. The question isn't if it fails, but how your system behaves when it does. If you just wrap your calls in a standard try-catch block and let the request time out every single time, you're essentially inviting a cascading failure. You'll tie up your own threads waiting for a response that isn't coming, and suddenly your entire application is unresponsive because one downstream payment gateway is having a bad Tuesday.

The Trap of the Simple Kill-Switch

The most intuitive way to handle this is usually what I call the "naive kill-switch." You create a boolean flag, isBroken. If a call fails, you set it to true. If it's true, you don't even try the call; you just return an error. It looks clean in a code review, but it's practically useless in production. Why? Because it has no way to recover. Once that flag is flipped, your system is effectively dead until someone manually restarts the JVM or you've written some awkward separate polling mechanism to flip it back.

// Don't do this in production
class NaiveBreaker {
  var isBroken = false

  def call[T](block: => T): T = {
    if (isBroken) throw new Exception("Circuit is open!")
    try {
      block
    } catch {
      case _: Exception => 
        isBroken = true 
        throw _
    }
  }
}


This approach is too binary. It doesn't account for intermittent network blips, and it doesn't provide a path back to health. In a real-world scenario, you don't want to shut down your integration because of one 500 error; you want to shut it down because it's failed 10 times in a row, and you want it to automatically try again after a few seconds to see if the service has recovered.

Managing State with a Finite State Machine

To do this right, we need a proper state machine. I usually think of a Circuit Breaker as having three distinct states: Closed (everything is fine), Open (stop trying, it's broken), and Half-Open (let's try one request to see if it's fixed). The transition from Open to Half-Open is the magic part—it's governed by a timer. After a specified "reset timeout," the breaker moves to Half-Open. If the next call succeeds, we close the circuit. If it fails, we swing right back to Open and reset the timer.

When implementing this in Scala, you have to be careful about concurrency. Since multiple threads will be hitting this breaker, using a simple var isn't going to cut it—you'll run into race conditions where the failure count is inconsistent. I prefer using an AtomicReference to hold the state, ensuring that transitions are thread-safe without locking the entire execution path.

The Trade-off: Throughput vs. Stability

Now, you might wonder why we bother with the "Half-Open" state instead of just periodically polling the service. The reason is resource preservation. By allowing only one request through during the Half-Open phase, we protect the struggling downstream service from a "thundering herd" effect. If a service is barely recovering and you suddenly slam it with 1,000 queued requests the moment the timer expires, you'll probably just knock it over again.

The cost of this pattern is a slight increase in complexity and a small amount of overhead for the state checks. But compared to the cost of a total system outage, it's a bargain. You're trading a few nanoseconds of CPU time for the ability to survive a partial system collapse.




📋 Practical Task

Exercise: Implementing a State-Based Circuit Breaker

Your task is to implement a CircuitBreaker class that manages the stability of a simulated external API call. You should not use any external libraries; implement the logic from scratch using Scala's standard library.

Requirements:

  • States: The breaker must support Closed, Open, and HalfOpen.
  • Failure Threshold: The circuit should transition from Closed to Open only after a configurable number of consecutive failures (e.g., 3 failures).
  • Reset Timeout: Once Open, the breaker must stay open for a specified duration (e.g., 5 seconds). After this time, the next call should put the breaker into the HalfOpen state.
  • Recovery: In the HalfOpen state, a single successful call must transition the breaker back to Closed. A single failure must transition it back to Open and reset the timeout.
  • Thread Safety: Ensure that the state transitions are thread-safe (hint: use java.util.concurrent.atomic.AtomicReference).

Starter Template:

case object Closed
case object Open
case object HalfOpen

class CircuitBreaker(failureThreshold: Int, resetTimeoutMillis: Long) {
  // Implement state management and the 'call' method here
  def call[T](block: => T): T = {
    ???
  }
}

// Test your implementation:
// 1. Call a failing function 3 times -> Verify it opens.
// 2. Call it immediately again -> Verify it fails fast without executing the block.
// 3. Wait 5 seconds -> Call it again -> Verify it attempts the block (Half-Open).
// 4. Make the block succeed -> Verify it returns to Closed.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.