Object Pool Pattern#
An object pool keeps a set of pre-initialized expensive objects (threads, connections, byte buffers, GPU contexts) and hands them out on demand. Borrowed objects return to the pool when done.
flowchart LR
R1([Requester]) -->|acquire| Pool[(Pool)]
R2([Requester]) -->|acquire| Pool
Pool --> O1[Obj 1]
Pool --> O2[Obj 2]
Pool --> O3[Obj 3]
R1 -. release .-> Pool
R2 -. release .-> Pool
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
class R1,R2 client;
class Pool cache;
class O1,O2,O3 service;
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
Examples: JDBC connection pool, thread pool, Netty ByteBuf pool, libuv worker pool, GameObject pools in Unity.
The object pool trades memory for time: keep N pre-created objects around so future requests amortize the cost. The pattern is invisible in well-implemented libraries and disastrous when rolled by hand.
When pooling pays off#
- Object creation cost is high (TCP+TLS handshake, JVM class load, GPU context).
- Throughput is high relative to working set (many requests, few simultaneous in-flight).
- Resource is bounded (DB max_connections, GPU memory).
When pooling hurts#
- Object creation is cheap; pooling adds bookkeeping overhead.
- Working set per request is large; pool keeps memory alive forever.
- Concurrency is unpredictable; pool starves under sudden bursts.
Lifecycle#
stateDiagram-v2
[*] --> Created : pool factory
Created --> Idle : warmup
Idle --> InUse : acquire()
InUse --> Idle : release()
Idle --> Destroyed : maxIdleTimeout / shutdown
InUse --> Destroyed : validation failed
Each pool implementation needs:
- create: how to make one.
- validate: is it still usable? (
SELECT 1,ping, etc.) - destroy: clean shutdown of the resource.
- passivate/activate (some pools): reset state between checkouts.
Borrowing API#
// Java commons-pool style
T obj = pool.borrowObject(); // may block until one is free
try {
use(obj);
} finally {
pool.returnObject(obj); // always return
}
// Idiomatic with try-with-resources
try (Connection conn = dataSource.getConnection()) {
use(conn); // auto-released on exit
}
The borrow API must handle:
- Timeout: bounded wait when pool is exhausted; throw rather than hang forever.
- Validation: optionally test before borrow / on return.
- Invalidate: caller-initiated "this thing is broken, don't reuse it".
Sizing#
Apply Little's law:
Caps come from the resource:
- DB has max_connections; pool can't exceed minus a small reserve.
- Thread pool can't exceed CPU's useful concurrency; past that, contention wins.
Leak detection#
A borrowed object that's never returned drains the pool. Production pools include:
- Borrow timeout: how long the next borrower waits.
- Leak detection: log stacktrace of any object held longer than
leakDetectionThreshold(HikariCP). - Max lifetime: force recycle after N minutes to dodge stale connections.
Anti-patterns#
- Pooling for cheap objects (java.util.Date, String): premature optimisation; GC handles it.
- Sharing pooled objects across threads without reset: state leaks.
- Unbounded pool: equivalent to no pool with worse profiling.
- Implementing your own: pools have subtle concurrency bugs; use a proven library.
Modern alternatives#
| Scenario | Old | Modern |
|---|---|---|
| Many blocking threads | Thread pool | Virtual threads (Java 21+), async/await |
| Allocations in hot loop | Object pool | Escape-analysis stack allocation, ZGC/Shenandoah |
| Network buffers | Per-request alloc | Netty / Tokio pooled byte buffers (still pool) |
Virtual threads and zero-copy buffers have replaced many ad-hoc object pools.
Where pools still rule#
- DB connection pools (HikariCP, PgBouncer)
- Network buffer pools (Netty
PooledByteBufAllocator) - Worker thread pools for blocking I/O
- Game-engine
GameObjectpools (bullets, particles) - GPU command-buffer pools
Where pools fit#
flowchart TB
OP((Object pool<br/>pattern))
CP[Connection pooling<br/>canonical large-scale]
CON[Concurrency primitives<br/>semaphore underlies]
CR[Creational patterns<br/>family]
AS[Async models<br/>virtual threads alt]
OP --> CP
CON --> OP
CR --> OP
AS -. modern alt .- OP
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class OP service;
class CP,CON,CR,AS datastore;
Glossary & fundamentals#
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Connection pooling | the canonical large-scale example | connection-pooling |
LLD |
Concurrency primitives | semaphore underlies many pools | concurrency-primitives |
LLD |
Creational patterns | the pattern family | creational-patterns |
LLD |
Async models | virtual threads are the modern alternative | async-models |
Quick reference#
Use when#
- Creation cost is high
- Resource bounded externally (DB max_connections)
- Throughput high; working set bounded
Skip when#
- Cheap objects (GC is fine)
- Unpredictable bursty load
- Working set ≈ total set
Lifecycle#
create → validate → idle → borrow → return → idle → destroy
Borrow API must support#
- Acquire timeout
- Validation on borrow / return
- Invalidate (broken object)
Sizing#
Cap below resource max (DB max_connections - reserve).Leak detection#
- Borrow timeout (~30s)
- Stacktrace log when held > threshold (HikariCP)
- Max lifetime force-recycle (30 min)
Anti-patterns#
- Pooling cheap objects
- Sharing without reset
- Unbounded pool
- DIY implementation (use library)
Modern alternatives#
| Old | New |
|---|---|
| Thread pool for blocking | Virtual threads (Java 21+) |
| Manual object pool | Escape-analysis, ZGC |
| Byte buffer pool | Pooled allocators (Netty/Tokio) |
Refs#
- Apache Commons Pool
- HikariCP wiki
- Netty PooledByteBufAllocator
- Refactoring Guru
FAQ#
What is the object pool pattern?#
It keeps a set of pre-created expensive objects ready to lend on demand. Callers borrow an object, use it, and return it instead of allocating one per request.
When should I use an object pool?#
When object creation is expensive (DB connections, threads, large buffers) and reuse is safe. For cheap immutable values an allocator is faster and simpler.
What is the difference between a connection pool and an object pool?#
A connection pool is just a specialised object pool whose lifecycle includes liveness checks, idle timeouts, and reconnect. The underlying pattern is the same.
How do I size an object pool correctly?#
Match the pool to your concurrency limit. Too small starves requests, too large wastes memory and overloads the downstream service. Measure and adjust under load.
What happens when an object pool is exhausted?#
Callers either block on a wait queue, fail fast with an error, or create new objects up to a hard ceiling. The policy depends on your latency and reliability goals.
Related Topics#
- Connection Pooling: the most common production object pool
- Concurrency Primitives: semaphores underlie pool wait semantics
- Async Models: virtual threads as the modern alternative
Further reading#
- Doc - Apache Commons Pool
- Doc - HikariCP - pool sizing
- Doc - Netty - PooledByteBufAllocator
- Blog - Refactoring Guru - Object Pool