Connection Pool Manager
Difficulty: Hard | Track: LLD
Problem statement
Design a thread-safe generic connection pool for database connections with bounded size, timeout, and health validation. The pool must manage a fixed maximum number of live connections, lending them to callers and reclaiming them after use. Idle connections that exceed a configurable lifetime should be automatically evicted by a background task.
Requirements
Functional:
- Pool bounded by
maxSize: never create more thanmaxSizeconnections simultaneously acquire(timeout): blocks until a connection is available or the timeout expires, returning the connection or throwingrelease(connection): return connection to the pool; validate before returning, discard and replace permit if unhealthy- Connections idle longer than
maxIdleTimeMsare evicted by a background task - Configurable via Builder:
maxSize,minIdle,maxWaitMs,validationQuery,maxIdleTimeMs - Pool pre-warms to
minIdleconnections on startup
Out of scope:
- Read replicas and connection sharding
- SSL / TLS configuration
- Connection-level authentication rotation
Key classes
// Wrapper around the raw connection
public class PooledConnection<T> {
private final T underlying;
private volatile Instant lastUsedAt;
private volatile boolean valid;
public PooledConnection(T underlying) { ... }
public T get() { return underlying; }
public Instant getLastUsedAt() { return lastUsedAt; }
public void touch() { this.lastUsedAt = Instant.now(); }
public boolean isValid() { return valid; }
public void invalidate() { this.valid = false; }
}
// Pool configuration
public class PoolConfig {
private final int maxSize;
private final int minIdle;
private final long maxWaitMs;
private final long maxIdleTimeMs;
private final String validationQuery;
private PoolConfig(Builder b) { ... }
public static class Builder {
public Builder maxSize(int n) { ... }
public Builder minIdle(int n) { ... }
public Builder maxWaitMs(long ms) { ... }
public Builder maxIdleTimeMs(long ms) { ... }
public Builder validationQuery(String q) { ... }
public PoolConfig build() { ... }
}
}
// Core pool
public class ConnectionPool<T> implements AutoCloseable {
private final Semaphore permits;
private final BlockingDeque<PooledConnection<T>> idlePool;
private final Supplier<T> connectionFactory;
private final Predicate<T> validator;
private final PoolConfig config;
private final ScheduledExecutorService evictionScheduler;
public ConnectionPool(PoolConfig config,
Supplier<T> connectionFactory,
Predicate<T> validator) { ... }
// semaphore.tryAcquire(timeout) → poll idle or create new → validate
public PooledConnection<T> acquire(long timeout, TimeUnit unit)
throws InterruptedException, TimeoutException { ... }
// validate → push to idlePool or semaphore.release()
public void release(PooledConnection<T> conn) { ... }
// called by EvictionTask: remove expired idle, release permit per removal
void evict() { ... }
@Override
public void close() { ... }
}
// Background eviction task
public class EvictionTask<T> implements Runnable {
private final ConnectionPool<T> pool;
public EvictionTask(ConnectionPool<T> pool) { ... }
@Override
public void run() {
pool.evict();
}
}
Patterns used
- Object Pool: Core pattern — reuse expensive-to-create connections instead of creating and destroying them per request
- Builder:
PoolConfig.Builderseparates optional configuration from construction, keeping the pool constructor clean - Strategy: The validator is injected as a
Predicate<T>, allowing callers to swap health-check logic without modifying the pool - Decorator:
PooledConnection<T>wraps the raw connectionTto attach pool-managed metadata (lastUsedAt,valid) without modifying the connection class - Template Method:
acquiredefines the fixed protocol (acquire permit → get or create → validate) while delegating connection creation and validation to injected collaborators
Key design decisions
- Semaphore as the gate for total connections: The
Semaphoreis the single source of truth for how many connections exist (idle + in-use). Every created connection consumes one permit; every discarded connection releases one. This prevents the pool from silently growing beyondmaxSizeeven under concurrent pressure. - BlockingDeque for idle connections: Using a deque (LIFO from the front) keeps recently-used connections warm in the DB server's cache, while still supporting FIFO draining during eviction.
- Validate before returning on
release: Returning a broken connection to the idle pool is a "poison pill" — the next caller acquires it, fails immediately, and the error is blamed on unrelated code. Discarding on release keeps the pool healthy. - Eviction must release the Semaphore permit: If eviction removes an idle connection but forgets to release the permit, the pool shrinks over time and eventually deadlocks. The permit lifecycle must mirror the connection lifecycle exactly.
acquireatomicity via Semaphore-first ordering: Acquiring the permit before polling the idle deque ensures two threads can never race to create the (maxSize+1)th connection.
What the interviewer is looking for
- Correct concurrency primitives: Choosing
Semaphore+BlockingDequeover simpler alternatives (e.g., a plainListwithsynchronized) and explaining why each one is needed - Permit lifecycle correctness: Demonstrating that every code path through
acquire,release, andevictkeeps permits consistent with live connections - Poison-pill prevention on release: Explicitly validating before re-enqueue and explaining the class of bugs it prevents
- Background eviction without leaks: Wiring
ScheduledExecutorServiceand shutting it down inclose(), including releasing permits for removed connections - Generic design: Using
ConnectionPool<T>withSupplier<T>andPredicate<T>to show the pool is reusable beyond JDBC