State Design Pattern: Let's build a circuit breaker
The state pattern is a behavioral software design pattern that allows an object to alter its behavior when its internal state changes. This pattern is close to the concept of finite-state machines. The state pattern can be interpreted as a strategy pattern, which is able to switch a strategy through invocations of methods defined in the pattern's interface.
The state pattern is used in computer programming to encapsulate varying behavior for the same object, based on its internal state. This can be a cleaner way for an object to change its behavior at runtime without resorting to conditional statements and thus improve maintainability.
Definition of Circuit breaker:
Circuit breaker is a design pattern used in software development. It is used to detect failures and encapsulates the logic of preventing a failure from constantly recurring, during maintenance, temporary external system failure or unexpected system difficulties.
gobreaker is a Go library that implements circuit breaker. I read the source code, and I saw that there are if/switch cases all over the code to handle each state. For instance this is part of the source code:
func (cb *CircuitBreaker) onSuccess(state State, now time.Time) {
switch state {
case StateClosed:
cb.counts.onSuccess()
case StateHalfOpen:
cb.counts.onSuccess()
if cb.counts.ConsecutiveSuccesses >= cb.maxRequests {
cb.setState(StateClosed, now)
}
}
}
func (cb *CircuitBreaker) onFailure(state State, now time.Time) {
switch state {
case StateClosed:
cb.counts.onFailure()
if cb.readyToTrip(cb.counts) {
cb.setState(StateOpen, now)
}
case StateHalfOpen:
cb.setState(StateOpen, now)
}
}
func (cb *CircuitBreaker) currentState(now time.Time) (State, uint64) {
switch cb.state {
case StateClosed:
if !cb.expiry.IsZero() && cb.expiry.Before(now) {
cb.toNewGeneration(now)
}
case StateOpen:
if cb.expiry.Before(now) {
cb.setState(StateHalfOpen, now)
}
}
return cb.state, cb.generation
}
I don't want to criticize this library. It is a great library and it's doing its job perfectly. I just wanted to use State pattern in a real-world example. That's why I rewrite it with State pattern. In the state pattern, all the logic and details for each state, encapsulated in its own object, eliminating the need for if/switch cases and consequently resulted in more readable code.
States in circuit breaker
The circuit breaker has 3 steps:
- Closed
- Open
- Half-open
The initial state is closed. In this state, all requests allow reaching the backend service. If the number of consecutive failures reached a certain threshold, it switches to the open state. The circuit remains in this state for a predefined period of time and rejects all the requests with an error, saving time and increase response time for a failed service. After this predefined time, the circuit goes to half-open. If a request fails in this state, it immediately goes to the open state again. Otherwise, it goes to the closed state (The number of successful requests in order to move to the closed state is configurable, the default is 1). The picture below depicts the idea:
State design pattern
In Golang we don't have inheritance. So instead of having a base class for all states, I declared all the common features for each state in an interface
:
type state interface {
execute(func() (interface{}, error)) (interface{}, error)
getCounts() Counts
getType() StateType
// for allocation resources
onEnter()
// for releasing resources
onLeave()
}
And we have 3 structs that implement this interface:
// These are states of CircuitBreaker.
var (
closed *closedState
halfOpen *halfOpenState
open *openState
)
Let's focus more on the state
interface. execute
method is the primary logic of the state. It accepts a function that returns eighter error
or result
and it executes this function. If an error occurs it counts as a failure, otherwise counts as successful and returns the result to the caller. Each state implements the execute logic differently. For instance, in the open
state, it just simply returns an error indicating that the circuit is open:
func (s *openState) execute(req func() (interface{}, error)) (interface{}, error) {
return nil, ErrOpenState
}
The execute
function for closed
and halfOpen
states have more logic.
getCounts()
returns statistics about the current state like total requests or total failure. One thing to bear in mind is that if the state changes for whatever reason, all counts will be reset to zero, and new generation will be started:
getType()
simply returns the state type. Either: Closed, Open or HalfOpen.
onEnter()
and onLeave()
methods are events fired for each state. Imaging a transition from closed
to open
:
First onLeave
event for closed
state fires, then onEnter
for open
state.
Changing the state
In the UML for state pattern, each state holds a reference to state context and they use this context to notify that state is changing. In my design:
type CircuitBreaker struct {
...
}
func (cb *CircuitBreaker) changeState(newState state) {
...
}
CircuitBreaker
is the state context and each state holds a reference to it:
func NewClosedState(cb *CircuitBreaker) *closedState
func NewHalfOpenState(cb *CircuitBreaker) *halfOpenState
func NewOpenState(cb *CircuitBreaker) *openState
Last word
The state design pattern is a great pattern when you have many states, and each state has a different logic. State pattern moves the logic for each state to its own class, eliminating the program from if conditions
and switch/cases
. You can find the full source code on the github page.