Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ A curated collection of idiomatic design & application patterns for Go language.

| Pattern | Description | Status |
|:-------:|:----------- |:------:|
| [Bulkheads](/stability/bulkhead.md) | Enforces a principle of failure containment (i.e. prevents cascading failures) | |
| [Bulkheads](/stability/bulkhead.md) | Enforces a principle of failure containment (i.e. prevents cascading failures) | |
| [Circuit-Breaker](/stability/circuit-breaker.md) | Stops the flow of the requests when requests are likely to fail | ✔ |
| [Deadline](/stability/deadline.md) | Allows clients to stop waiting for a response once the probability of response becomes low (e.g. after waiting 10 seconds for a page refresh) | |
| [Fail-Fast](/stability/fail_fast.md) | Checks the availability of required resources at the start of a request and fails if the requirements are not satisfied | |
| [Handshaking](/stability/handshaking.md) | Asks a component if it can take any more load, if it can't, the request is declined | |
| [Steady-State](/stability/steady_state.md) | For every service that accumulates a resource, some other service must recycle that resource | |
| [Deadline](/stability/deadline.md) | Allows clients to stop waiting for a response once the probability of response becomes low (e.g. after waiting 10 seconds for a page refresh) | |
| [Fail-Fast](/stability/fail_fast.md) | Checks the availability of required resources at the start of a request and fails if the requirements are not satisfied | |
| [Handshaking](/stability/handshaking.md) | Asks a component if it can take any more load, if it can't, the request is declined | |
| [Steady-State](/stability/steady_state.md) | For every service that accumulates a resource, some other service must recycle that resource | |

## Profiling Patterns

Expand Down
83 changes: 83 additions & 0 deletions stability/bulkhead.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Bulkhead Pattern

The bulkhead pattern is inspired by the sectioned partitions (bulkheads) of a
ship's hull. If one section is breached, only that section floods — the rest of
the ship stays afloat. In software, the pattern isolates elements of an
application into pools so that if one fails, the others continue to function.

By partitioning resource access (e.g. connection pools, goroutine pools, or
semaphores), the bulkhead pattern prevents a single failing component from
consuming all resources and cascading into a system-wide outage.

## Implementation

Below is a `Bulkhead` that limits concurrent access to a downstream service
using a buffered channel as a semaphore.

```go
package bulkhead

import (
"errors"
"time"
)

var (
ErrBulkheadFull = errors.New("bulkhead capacity full")
)

// Bulkhead limits the number of concurrent calls to a function.
type Bulkhead struct {
sem chan struct{}
timeout time.Duration
}

// New creates a Bulkhead with the given maximum concurrent capacity and a
// timeout for acquiring a slot.
func New(capacity int, timeout time.Duration) *Bulkhead {
return &Bulkhead{
sem: make(chan struct{}, capacity),
timeout: timeout,
}
}

// Execute runs fn if a slot is available within the configured timeout.
// If the bulkhead is full it returns ErrBulkheadFull without executing fn.
func (b *Bulkhead) Execute(fn func() error) error {
select {
case b.sem <- struct{}{}:
defer func() { <-b.sem }()
return fn()
case <-time.After(b.timeout):
return ErrBulkheadFull
}
}
```

## Usage

```go
orderBulkhead := bulkhead.New(10, 1*time.Second)
paymentBulkhead := bulkhead.New(5, 1*time.Second)

// The order service is isolated from the payment service.
// If payments exhaust their 5 slots, orders can still proceed
// with their independent pool of 10.
err := orderBulkhead.Execute(func() error {
return orderService.Place(order)
})

err = paymentBulkhead.Execute(func() error {
return paymentService.Charge(order)
})

if errors.Is(err, bulkhead.ErrBulkheadFull) {
log.Println("service is at capacity, try again later")
}
```

## Rules of Thumb

- Size each bulkhead based on the downstream service's capacity and expected latency.
- Combine with the circuit breaker pattern: a bulkhead limits concurrency while a circuit breaker stops calls to an already-failing service.
- Monitor bulkhead rejection rates — a consistently full bulkhead indicates the pool is undersized or the downstream is too slow.
74 changes: 74 additions & 0 deletions stability/deadline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Deadline Pattern

The deadline pattern allows a client to stop waiting for a response once a
specified amount of time has passed, at which point the probability of a
successful response becomes too low to be useful. This avoids tying up resources
indefinitely on slow or unresponsive operations.

In Go, the `context` package provides first-class support for deadlines and
timeouts, making it the idiomatic way to implement this pattern.

## Implementation

```go
package deadline

import (
"context"
"time"
)

// Work represents a unit of work that respects context cancellation.
type Work func(ctx context.Context) error

// WithDeadline wraps a unit of work with a deadline. If the work does not
// complete before the deadline, the context is cancelled and an error is
// returned.
func WithDeadline(timeout time.Duration, work Work) Work {
return func(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

done := make(chan error, 1)

go func() {
done <- work(ctx)
}()

select {
case err := <-done:
return err
case <-ctx.Done():
return ctx.Err()
}
}
}
```

## Usage

```go
slowOperation := func(ctx context.Context) error {
select {
case <-time.After(5 * time.Second):
fmt.Println("operation completed")
return nil
case <-ctx.Done():
return ctx.Err()
}
}

// Wrap the slow operation with a 2-second deadline.
wrapped := deadline.WithDeadline(2*time.Second, slowOperation)

err := wrapped(context.Background())
if err != nil {
fmt.Println(err) // context deadline exceeded
}
```

## Rules of Thumb

- Always propagate the `context.Context` into downstream calls so that cancellation reaches every layer.
- Choose deadline values based on observed latency percentiles (e.g. p99) rather than arbitrary round numbers.
- Prefer `context.WithTimeout` for relative durations and `context.WithDeadline` for absolute wall-clock deadlines.
90 changes: 90 additions & 0 deletions stability/fail_fast.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Fail-Fast Pattern

The fail-fast pattern checks the availability of required resources at the start
of a request and fails immediately if the requirements are not satisfied. Rather
than performing expensive work only to discover a missing dependency halfway
through, the request is rejected early with a clear error.

This reduces wasted computation, frees resources faster, and gives callers
immediate feedback so they can retry or fall back.

## Implementation

```go
package failfast

import "errors"

// Checker validates whether a required precondition is met.
type Checker func() error

// Handler represents the actual business logic to execute.
type Handler func() error

// FailFast verifies all preconditions before invoking the handler.
// If any check fails, it returns immediately without calling the handler.
func FailFast(handler Handler, checks ...Checker) error {
for _, check := range checks {
if err := check(); err != nil {
return err
}
}

return handler()
}
```

A concrete example — a request handler that validates a database connection and
cache availability before processing:

```go
package failfast

import "errors"

var (
ErrDBUnavailable = errors.New("database is unavailable")
ErrCacheUnavailable = errors.New("cache is unavailable")
)

func CheckDB(db *sql.DB) Checker {
return func() error {
if err := db.Ping(); err != nil {
return ErrDBUnavailable
}
return nil
}
}

func CheckCache(cache CacheClient) Checker {
return func() error {
if !cache.IsAlive() {
return ErrCacheUnavailable
}
return nil
}
}
```

## Usage

```go
err := failfast.FailFast(
func() error {
// Expensive business logic that requires both DB and cache.
return processOrder(order)
},
failfast.CheckDB(db),
failfast.CheckCache(cache),
)

if err != nil {
log.Printf("request rejected early: %v", err)
}
```

## Rules of Thumb

- Only check preconditions that are cheap to verify (pings, health flags). The goal is to fail fast, not to add latency.
- Combine with the circuit breaker pattern: a circuit breaker remembers previous failures, while fail-fast verifies current readiness.
- Return specific error types so callers can distinguish between "not ready" and "processing failed."
102 changes: 102 additions & 0 deletions stability/handshaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Handshaking Pattern

The handshaking pattern allows a component to ask another component whether it
can accept more load before sending actual work. If the target component signals
that it is at capacity, the request is declined without even attempting the
operation. This protects both the caller and the callee from being overwhelmed.

Unlike the circuit breaker, which reacts to failures after they happen,
handshaking is a proactive, cooperative mechanism — the service itself
advertises its readiness.

## Implementation

```go
package handshaking

import "errors"

var (
ErrServiceAtCapacity = errors.New("service is at capacity")
)

// Service represents a downstream component that supports health negotiation.
type Service interface {
// IsReady reports whether the service can accept new work.
IsReady() bool

// Do performs the actual work.
Do(req Request) (Response, error)
}

// Request and Response are domain-specific types.
type Request struct {
Payload interface{}
}

type Response struct {
Result interface{}
}

// Call performs a handshake with the target service before sending the request.
// If the service is not ready, it returns ErrServiceAtCapacity immediately.
func Call(svc Service, req Request) (Response, error) {
if !svc.IsReady() {
return Response{}, ErrServiceAtCapacity
}

return svc.Do(req)
}
```

A concrete service implementation using active connection tracking:

```go
package handshaking

import "sync/atomic"

type TrackedService struct {
active int64
capacity int64
handler func(Request) (Response, error)
}

func NewTrackedService(capacity int64, handler func(Request) (Response, error)) *TrackedService {
return &TrackedService{
capacity: capacity,
handler: handler,
}
}

func (s *TrackedService) IsReady() bool {
return atomic.LoadInt64(&s.active) < s.capacity
}

func (s *TrackedService) Do(req Request) (Response, error) {
atomic.AddInt64(&s.active, 1)
defer atomic.AddInt64(&s.active, -1)

return s.handler(req)
}
```

## Usage

```go
svc := handshaking.NewTrackedService(100, func(req handshaking.Request) (handshaking.Response, error) {
result, err := processWork(req.Payload)
return handshaking.Response{Result: result}, err
})

resp, err := handshaking.Call(svc, handshaking.Request{Payload: data})
if errors.Is(err, handshaking.ErrServiceAtCapacity) {
log.Println("service busy, back off and retry later")
}
```

## Rules of Thumb

- The `IsReady` check must be cheap — it should read a counter or flag, not run diagnostics.
- Handshaking works best for in-process or sidecar communication. For remote services, consider a health-check endpoint that returns HTTP 503 when at capacity.
- Combine with retry and backoff logic on the caller side to handle transient capacity limits gracefully.
Loading