Problem framing
Design a connection pool for expensive resources (database connections, HTTP clients, gRPC channels): callers acquire() a ready connection, use it, then release() back to the pool instead of opening a new TCP+TLS session per request. Tests object lifecycle, bounded concurrency, and thread-safe blocking — the LLD counterpart to "why is HikariCP fast?"
| Class | Responsibility |
|---|---|
ConnectionPool | acquire(timeout), release(conn), owns idle + in-use sets |
PoolConfig | maxSize, minIdle, maxIdleTimeMs, acquireTimeoutMs |
PooledConnection | Wrapper: raw connection + lastUsedAt, valid flag |
ConnectionFactory | create(), validate(conn), destroy(conn) |
PoolMetrics (optional) | active, idle, wait count — observability hook |
Lifecycle
acquire():
if idle queue not empty → pop, validate, return (or destroy if stale)
else if in-use count < maxSize → factory.create(), return
else block until release() or timeout
release(conn):
if invalid or idle time exceeded → destroy
else reset state (rollback txn, clear session) → push to idle queue
notify one blocked acquirerFactory isolates driver-specific code (JDBC URL, credentials) from pool logic — same pattern as Executor + ThreadFactory.
Thread-safety
| Approach | Trade-off |
|---|---|
BlockingQueue<PooledConnection> | Simple acquire/release; size enforced by counters |
Semaphore(maxSize) + set | Limits in-use; idle stored separately |
| Per-connection mutex | Fine-grained; harder to reason about |
Single lock on pool metadata is acceptable in interviews if you mention contention cost. Double-release is a bug — mark connection inUse or use wrapper tokens.
Health and eviction
Validate on checkout — SELECT 1 or socket ping; discard dead connections. Idle eviction — background thread removes connections idle > maxIdleTimeMs (HikariCP "housekeeping"). Max lifetime — rotate connections after N minutes regardless of idle (credential rotation, load balancer stickiness).
Senior-level signal
Contrast with per-request new connection: TLS handshake + auth latency dominates at high QPS. State pool sizing: connections ≈ ((core_count * 2) + effective_spindle_count) is a JDBC rule of thumb — cite as awareness, not gospel.
Distributed pool (PgBouncer, RDS Proxy) is out of scope — "this class is the in-process pool; proxy is another tier." Link to Concurrency roadmap: acquire blocking resembles semaphore + condition variable.
Common pitfalls
Returning a connection after caller forgot to rollback transaction — pool must reset() on release. Leak when caller never releases — mention try/finally or wrapper that auto-releases (scope guard / try-with-resources).
Where this goes next
Skip List / Ordered Set moves from lifecycle management to a class that has to stay correct under concurrent structural mutation — a different flavor of the thread-safety problem this topic keeps returning to.
Further Reading
- Educative — Grokking LLD: Design a Connection Pool (pool lifecycle, factory, health check)Course35m
- Refactoring.Guru — Object Pool pattern (reuse expensive objects, reset vs recreate trade-offs)Reference15m
- HikariCP README — Connection pool sizing and lifecycle (production reference implementation)Reference15m
Practice Tasks (Optional)
Design or implement locally in any language — no autograding. Focus on class structure, extensibility, and being able to explain trade-offs out loud.
- Design ConnectionPool API45m
Classes: ConnectionPool, PoolConfig, PooledConnection, ConnectionFactory. API: acquire(timeout), release(conn). Walk through: pool size 3, 4 concurrent acquirers — one blocks until release. Note health-check on checkout and max idle TTL.