Strategy pattern for load balancing
The decision
The load balancing algorithm is decoupled from the coreLoadBalancer class using the strategy pattern. The LoadBalancer accepts any strategy that implements a pick() method.
Why this matters
This design makes it trivial to add new load balancing algorithms without modifying existing code. Want to add Least Connections, Weighted Round Robin, or IP Hash? Just create a new strategy class and inject it.Implementation
- LoadBalancer (strategy consumer)
- RoundRobin (strategy implementation)
- Usage
Benefits
Extensibility
Extensibility
Adding a new algorithm requires zero changes to existing code. Just implement a new class with a
pick() method.Testability
Testability
The
LoadBalancer can be tested with mock strategies, and each strategy can be unit tested independently.Single responsibility
Single responsibility
The
LoadBalancer orchestrates components; the strategy implements the algorithm. Each class has one reason to change.This is a textbook application of the Open/Closed Principle: the system is open for extension (new strategies) but closed for modification (no changes to
LoadBalancer).Parallel health checks with AbortController timeouts
The decision
Health checks for all backends run concurrently usingPromise.all(), and each individual check has a 3-second timeout enforced by AbortController.
Why this matters
Without parallelization, a single slow backend would block health checks for all other backends. Without timeouts, a dead backend could hang indefinitely.Implementation
Performance comparison
- Sequential (bad)
- Parallel (good)
With 3 backends:
- Backend 1: 2s
- Backend 2: 5s (timeout)
- Backend 3: 1s
Benefits
Parallel health checks with timeouts ensure that health evaluation completes quickly and predictably, even when backends are slow or unresponsive.
Automatic failover mechanism
The decision
When a backend fails during request proxying, it is immediately marked unhealthy and removed from the rotation. No manual intervention is needed.Why this matters
This prevents cascading failures. Without immediate failover, the load balancer would continue sending traffic to a dead backend, causing more 502 errors.Implementation
Failover happens in two places:1. During proxy error
1. During proxy error
2. During health checks
2. During health checks
Recovery mechanism
Once theHealthChecker confirms a backend is responding successfully, it’s automatically re-added:
Benefits
This design achieves self-healing infrastructure: the system automatically adapts to backend failures and recoveries without operator intervention.
Separation of concerns
The decision
Each module has a single responsibility and a clear interface. No module knows about the implementation details of other modules.Why this matters
This makes the codebase:- Easier to understand: Each file has a clear purpose
- Easier to test: Each module can be tested in isolation
- Easier to extend: Changes to one module rarely affect others
Module responsibilities
| Module | Single Responsibility | What it does NOT do |
|---|---|---|
BackendPool | Manages backend registry and health state | Does not know about routing algorithms or health checking logic |
RoundRobin | Implements the routing algorithm | Does not know about health checking or proxying |
LoadBalancer | Orchestrates strategy + pool to pick a backend | Does not implement algorithms or manage state |
ProxyHandler | Forwards requests and handles proxy errors | Does not implement load balancing or health checking |
HealthChecker | Periodically verifies backend availability | Does not know about request routing or proxying |
Logger | Structured, categorized log output | Does not make decisions or change system state |
Dependency flow
- Dependencies are injected (not created inside modules)
- Each module receives only what it needs
- The composition happens at the top level (index.ts)
Benefits
- Testability
- Maintainability
- Reusability
You can test
LoadBalancer with a mock BackendPool and mock strategy. You can test HealthChecker with a mock BackendPool. Each module can be unit tested independently.This architecture follows the SOLID principles, particularly the Single Responsibility Principle and Dependency Inversion Principle.
Summary
These design decisions work together to create a system that is:- Extensible: Add new features without modifying existing code
- Resilient: Automatically handles failures and recoveries
- Observable: Structured logging for debugging and monitoring
- Testable: Each component can be tested independently
- Maintainable: Clear responsibilities and minimal coupling