Skip to content

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:

poolSize ≈ throughput × meanServiceTime + headroom

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 GameObject pools (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#

pool ≈ throughput × mean_service_time + headroom
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.

Further reading#