# Bulkhead Pattern

## Problem

When a system makes calls to multiple dependencies through a shared resource pool (e.g., a single thread pool or connection pool), one slow or failing dependency can consume all available resources. This starves healthy dependencies, causing total system failure — even though only one component is misbehaving.

## Solution

Partition resources into isolated compartments (bulkheads), just like watertight compartments in a ship's hull. Each dependency or critical operation gets its own dedicated pool with a fixed capacity. When one compartment fills up, excess requests to that dependency are rejected immediately, while other dependencies continue operating normally.

## When to Use

- Systems calling multiple external services through shared resources
- When one slow dependency has caused cascading failures before
- High-availability systems where partial degradation is preferred over total failure
- Microservice architectures with many downstream dependencies

## When to Avoid

- Simple applications with only one external dependency
- Extremely low-traffic systems where resource contention is unlikely
- When all dependencies are equally critical and share the same failure modes
- Prototypes or MVPs where the operational overhead isn't justified

## Pseudocode

```
class Bulkhead:
    max_concurrent = N
    active_count = 0

    function execute(operation):
        if active_count >= max_concurrent:
            raise BulkheadFullError

        active_count += 1
        try:
            return operation()
        finally:
            active_count -= 1
```

## Python Implementation

```python
import threading
import time


class BulkheadFullError(Exception):
    pass


class Bulkhead:
    def __init__(self, name: str, max_concurrent: int):
        self.name = name
        self.max_concurrent = max_concurrent
        self._semaphore = threading.Semaphore(max_concurrent)
        self._active = 0
        self._lock = threading.Lock()

    def execute(self, operation):
        acquired = self._semaphore.acquire(blocking=False)
        if not acquired:
            raise BulkheadFullError(
                f"Bulkhead '{self.name}' full ({self.max_concurrent}/{self.max_concurrent})"
            )
        with self._lock:
            self._active += 1
        try:
            return operation()
        finally:
            with self._lock:
                self._active -= 1
            self._semaphore.release()

    @property
    def active(self):
        with self._lock:
            return self._active


# --- Demo ---
service_a_bulkhead = Bulkhead("ServiceA", max_concurrent=2)
service_b_bulkhead = Bulkhead("ServiceB", max_concurrent=2)

results = []
results_lock = threading.Lock()


def slow_service_a(call_id):
    def op():
        print(f"  [A] Call {call_id} started")
        time.sleep(0.3)
        return f"A-result-{call_id}"
    return op


def fast_service_b(call_id):
    def op():
        print(f"  [B] Call {call_id} started")
        time.sleep(0.05)
        return f"B-result-{call_id}"
    return op


def run_in_bulkhead(bulkhead, operation, call_id):
    try:
        result = bulkhead.execute(operation)
        with results_lock:
            results.append(f"OK: {result}")
        print(f"  [{bulkhead.name}] Call {call_id} completed: {result}")
    except BulkheadFullError as e:
        with results_lock:
            results.append(f"REJECTED: {e}")
        print(f"  [{bulkhead.name}] Call {call_id} REJECTED: {e}")


# Launch 4 calls to A (only 2 slots) and 2 calls to B (2 slots)
threads = []
for i in range(1, 5):
    t = threading.Thread(target=run_in_bulkhead,
                         args=(service_a_bulkhead, slow_service_a(i), i))
    threads.append(t)

for i in range(1, 3):
    t = threading.Thread(target=run_in_bulkhead,
                         args=(service_b_bulkhead, fast_service_b(i), i))
    threads.append(t)

for t in threads:
    t.start()
time.sleep(0.05)  # Let threads start

for t in threads:
    t.join()

print(f"\nTotal results: {len(results)}")
ok_count = sum(1 for r in results if r.startswith("OK"))
rejected_count = sum(1 for r in results if r.startswith("REJECTED"))
print(f"  Succeeded: {ok_count}")
print(f"  Rejected:  {rejected_count}")
print("Service B was NOT affected by Service A's congestion.")
```

**Output (order may vary):**
```
  [A] Call 1 started
  [A] Call 2 started
  [ServiceA] Call 3 REJECTED: Bulkhead 'ServiceA' full (2/2)
  [ServiceA] Call 4 REJECTED: Bulkhead 'ServiceA' full (2/2)
  [B] Call 1 started
  [B] Call 2 started
  [ServiceB] Call 1 completed: B-result-1
  [ServiceB] Call 2 completed: B-result-2
  [ServiceA] Call 1 completed: A-result-1
  [ServiceA] Call 2 completed: A-result-2

Total results: 6
  Succeeded: 4
  Rejected:  2
Service B was NOT affected by Service A's congestion.
```

## Go Implementation

```go
package main

import (
	"fmt"
	"sync"
	"time"
)

type Bulkhead struct {
	name string
	sem  chan struct{}
}

func NewBulkhead(name string, maxConcurrent int) *Bulkhead {
	return &Bulkhead{
		name: name,
		sem:  make(chan struct{}, maxConcurrent),
	}
}

func (b *Bulkhead) Execute(operation func() string) (string, error) {
	select {
	case b.sem <- struct{}{}:
		defer func() { <-b.sem }()
		return operation(), nil
	default:
		return "", fmt.Errorf("bulkhead '%s' full", b.name)
	}
}

func main() {
	bulkheadA := NewBulkhead("ServiceA", 2)
	bulkheadB := NewBulkhead("ServiceB", 2)

	var wg sync.WaitGroup
	var mu sync.Mutex
	okCount, rejectedCount := 0, 0

	call := func(bh *Bulkhead, id int, dur time.Duration) {
		defer wg.Done()
		result, err := bh.Execute(func() string {
			fmt.Printf("  [%s] Call %d started\n", bh.name, id)
			time.Sleep(dur)
			return fmt.Sprintf("%s-result-%d", bh.name, id)
		})
		mu.Lock()
		defer mu.Unlock()
		if err != nil {
			rejectedCount++
			fmt.Printf("  [%s] Call %d REJECTED: %v\n", bh.name, id, err)
		} else {
			okCount++
			fmt.Printf("  [%s] Call %d completed: %s\n", bh.name, id, result)
		}
	}

	// 4 calls to A (2 slots), 2 calls to B (2 slots)
	for i := 1; i <= 4; i++ {
		wg.Add(1)
		go call(bulkheadA, i, 300*time.Millisecond)
	}
	for i := 1; i <= 2; i++ {
		wg.Add(1)
		go call(bulkheadB, i, 50*time.Millisecond)
	}

	wg.Wait()
	fmt.Printf("\nSucceeded: %d, Rejected: %d\n", okCount, rejectedCount)
	fmt.Println("Service B was NOT affected by Service A's congestion.")
}
```

## JavaScript Implementation

```javascript
class BulkheadFullError extends Error {
  constructor(name, max) {
    super(`Bulkhead '${name}' full (${max}/${max})`);
  }
}

class Bulkhead {
  constructor(name, maxConcurrent) {
    this.name = name;
    this.maxConcurrent = maxConcurrent;
    this.active = 0;
  }

  async execute(operation) {
    if (this.active >= this.maxConcurrent) {
      throw new BulkheadFullError(this.name, this.maxConcurrent);
    }
    this.active++;
    try {
      return await operation();
    } finally {
      this.active--;
    }
  }
}

// --- Demo ---
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

const bulkheadA = new Bulkhead("ServiceA", 2);
const bulkheadB = new Bulkhead("ServiceB", 2);

async function callService(bh, id, delayMs) {
  try {
    const result = await bh.execute(async () => {
      console.log(`  [${bh.name}] Call ${id} started`);
      await sleep(delayMs);
      return `${bh.name}-result-${id}`;
    });
    console.log(`  [${bh.name}] Call ${id} completed: ${result}`);
    return { ok: true };
  } catch (e) {
    console.log(`  [${bh.name}] Call ${id} REJECTED: ${e.message}`);
    return { ok: false };
  }
}

(async () => {
  const promises = [
    ...Array.from({ length: 4 }, (_, i) => callService(bulkheadA, i + 1, 300)),
    ...Array.from({ length: 2 }, (_, i) => callService(bulkheadB, i + 1, 50)),
  ];
  const results = await Promise.all(promises);
  const ok = results.filter((r) => r.ok).length;
  const rejected = results.filter((r) => !r.ok).length;
  console.log(`\nSucceeded: ${ok}, Rejected: ${rejected}`);
  console.log("Service B was NOT affected by Service A's congestion.");
})();
```

## Related Patterns

- **Circuit Breaker** — circuit breaker stops calls to a failed service; bulkhead limits how many concurrent calls can be in-flight to any one service.
- **Thread Pool** — a thread pool is a common implementation mechanism for a bulkhead; the pool size defines the bulkhead capacity.
- **Rate Limiter** — rate limiter controls the request rate; bulkhead controls concurrent inflight requests.
