Beginner14 min readDesign Patternslive prototype

Object Pool

Some objects are slow and expensive to build — like a database connection that needs a network handshake. Instead of making a new one every time and throwing it away, an Object Pool keeps a small set of ready-made ones that you borrow, use, and return, so the same few get reused over and over.

The idea

What it is

An Object Pool keeps a small set of ready-made, reusable objects and lends them out. When you need one, you borrow it from the pool; when you're done, you return it so the next person can reuse it. You never build a fresh one for every request. The whole point is to avoid the cost of creating and destroying expensive objects again and again.

Think of a car rental company. Renting a car is expensive to make — you have to buy it, register it, insure it. So the company doesn't build you a brand-new car every time you show up and then scrap it when you're done. That would be insane. Instead they keep a fixed fleet of ready cars. You take one from the lot, drive it, and bring it back — then the next customer drives the same car. If the whole fleet is out, you wait until someone returns one. An Object Pool works exactly like that rental lot, just with database connections instead of cars.

The one sentence to remember

Don't build a new expensive object for every request — keep a small pool and acquire → use → release the same few over and over. The number of objects actually created stays capped at the pool size, no matter how many requests come through.

What makes an object "expensive"?

A database connection has to open a network socket, do a TCP handshake, authenticate a username and password, and set up a session — that can take tens or hundreds of milliseconds before you run a single query. Threads, large memory buffers, and network sockets are expensive in the same way. Reusing one you already paid for is far cheaper than paying that cost again.

Mechanics

How it works

The three-step lifecycle: acquire → use → release

Every pooled object goes around the same little loop. Learn these three words and you know the pattern:

  • Acquire — ask the pool for an object. If a free one is sitting in the pool, you get it instantly (no creation cost). If the pool is empty and it hasn't hit its size limit yet, it creates one for you. If it's at the limit and all are busy, your request waits.
  • Use — do your work with the borrowed object: run your queries, send your bytes, fire your bullet. During this time the object is marked in-use so nobody else grabs the same one.
  • Release — give it back to the pool (usually after a quick reset to clear any leftover state). It's now available again, and if someone was waiting, the pool hands it straight to them.

Reuse is the whole idea — objects created stays capped

This is the number to watch. Serve 5 requests, 50, or 5,000 through a pool of 3, and you still only ever created 3 connections. The pool recycles the same few. Without a pool, 5,000 requests means 5,000 expensive new Connection() calls (and 5,000 handshakes). With a pool of 3, it means 3. That gap is why the pattern exists.

What happens when the pool runs out

A pool has a fixed maximum size — say 3 connections. That cap is deliberate: it protects the database from being flooded by thousands of connections at once. So what happens when all 3 are borrowed and a 4th request arrives? There are a few common choices, and real pools let you pick:

  • Wait (block) — the request pauses in a waiting queue until someone releases a connection, then it gets that one. This is the most common default. (The prototype does this.)
  • Grow — temporarily create extra objects above the normal size, then trim back later. Flexible, but you lose the hard cap that protected the resource.
  • Reject / time out — refuse the request (or wait only up to a limit, then throw). This keeps the system from piling up an endless backlog under overload.
Client needs a conn acquire release (reset) POOL · max 3 🟢 conn #1 · available 🟡 conn #2 · in-use 🟡 conn #3 · in-use all busy waiting… req #4 queued
The client acquires a connection from the pool, marking it 🟡 in-use while it works, then releases it (after a reset) back to 🟢 available. The pool holds at most 3. When all three are in-use, a new request drops into the waiting queue and stays there until someone releases — then it gets that freed connection.

Why "without a pool" goes wrong

Without a pool, every request runs new Connection(), pays the full handshake cost, uses the connection once, and throws it away. Under load — say a website with hundreds of requests a second — you're constantly opening and closing connections, hammering the database with handshakes, and burning time and memory on setup you immediately discard. The pool fixes this by paying the creation cost a tiny number of times up front and then reusing those objects forever. The prototype lets you feel the difference: in the WITHOUT world the created-count runs away with every click, while in the WITH world it flatlines at 3.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

Two worlds with the same clicking. Flip between WITHOUT Pool and WITH Pool. In WITHOUT Pool, every + Acquire runs new Connection() — you pay a 200ms handshake and the "expensive objects created" counter climbs by one every single click and never stops. In WITH Pool, there's a fixed pool of 3 connections shown as slots: 🟢 available or 🟡 in-use. + Acquire hands you an existing free slot (it turns 🟡) and the created counter flatlines at 3 no matter how many times you click — you're reusing, not building. Acquire a 4th time with all three busy and your request drops into a waiting queue ("pool exhausted — waiting…"); click Release on any in-use slot and the pool hands that freed connection straight to the waiting request. The scoreboard says it all: served 12 requests · created only 3. One fixed explain panel narrates every action; nothing scrolls away.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Watch the counter run away without a pool

Open the prototype on WITHOUT Pool. Click + Acquire several times. Each click runs new Connection(), pays a 200ms handshake, and bumps "expensive objects created" by one — 1, 2, 3, 4, 5… it never stops, because every request builds a fresh connection and then discards it. Notice there's no reuse at all: the number of objects created equals the number of clicks.

try 02

Flip to WITH Pool and see it flatline

Switch the toggle to WITH Pool. Now three connection slots appear, all 🟢 available. Click + Acquire and one slot turns 🟡 in-use — but the created counter stays at 3. Keep clicking Acquire and Release: connections flip between green and yellow, the scoreboard shows "served N requests · created only 3", yet no new object is ever built. That flat created-count is the entire lesson.

try 03

Exhaust the pool and hand off to a waiter

Still in WITH Pool, click + Acquire three times so all three slots are 🟡 in-use. Acquire once more — your request can't get a connection, so it drops into the waiting queue with "pool exhausted — waiting…". Now click Release on any in-use slot: instead of going back to green, that freed connection is handed straight to the waiting request. That wait-then-handoff is exactly how a real database pool protects the database while still serving everyone.

In practice

When to use it — and what trips people up

Reach for a pool when creation is genuinely expensive and reused often

An Object Pool pays off in a narrow but important set of cases — where building the object is slow or heavy, and you'll need many of them over and over:

  • Database connection pools — the classic case. Opening a DB connection is slow (network + auth), and a busy app needs one for every request. Real libraries: HikariCP (Java), SQLAlchemy's pool (Python), pgbouncer (Postgres).
  • Thread pools — creating an OS thread is costly, so a thread pool keeps a fixed set of worker threads and feeds them tasks instead of spawning a thread per task.
  • Network sockets / HTTP connections — reusing a kept-alive connection avoids repeating the TCP + TLS handshake for every call.
  • Large reusable buffers — big byte buffers or arrays that are pricey to allocate and clear can be pooled and handed out for each operation.
  • Game objects — bullets, particles, and enemies are spawned and destroyed constantly; pooling them avoids allocation spikes and garbage-collection stutter during play.

Don't pool cheap objects

Pooling only helps when creation is genuinely expensive and objects are reused a lot. Pooling small, cheap objects (a point, a short string) usually costs more than it saves — you add complexity and, in garbage-collected languages like Java or JavaScript, a pool of live objects can actually hurt the collector more than just letting short-lived objects be born and die. If new X() is fast, skip the pool.

What it gives you

  • Avoids repeated expensive creation — you pay the handshake/allocation cost only a few times (up to the pool size), then reuse those objects for every request after.
  • Caps resource usage — a fixed maximum protects the database (or OS) from being flooded by thousands of simultaneous connections or threads.
  • Smooths performance — no allocation spikes or handshake latency on the hot path; borrowing a ready object is near-instant.
  • Predictable under load — when demand exceeds the pool, requests queue in an orderly way instead of overwhelming the downstream resource.

Common mistakes

  • Added complexity — you now manage acquire/release, a size limit, waiting, and object lifecycle, instead of a simple new/discard.
  • You MUST release — an object that's borrowed and never returned (a leak) is gone from the pool forever; enough leaks and the pool starves and every request hangs waiting.
  • Stale state bugs — a pooled object carries whatever state the last user left on it; if you forget to reset it on release, the next caller sees leftover data.
  • Sizing is a tuning problem — too small and requests wait; too large and you waste resources or flood the database. The right size takes measurement.
  • Can hurt in GC languages — pooling cheap or short-lived objects keeps them alive longer and can make garbage collection worse, not better.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// A tiny fixed-size pool of expensive Connection objects.
class Connection {
  constructor(public readonly id: number) {
    // Imagine a slow network handshake + auth here (~200ms).
  }
  query(sql: string): void { /* ...use the connection... */ }
  reset(): void { /* clear leftover state before reuse */ }
}

class ConnectionPool {
  private available: Connection[] = [];   // free, ready to hand out
  private inUse = new Set<Connection>();   // currently borrowed
  private created = 0;                     // how many we ever built

  constructor(private readonly max: number) {}

  acquire(): Connection | null {
    let conn = this.available.pop();       // reuse a free one if we have it
    if (!conn && this.created < this.max) {
      conn = new Connection(++this.created); // under the cap? build one
    }
    if (!conn) return null;                // pool exhausted → wait or reject
    this.inUse.add(conn);
    return conn;
  }

  release(conn: Connection): void {
    this.inUse.delete(conn);
    conn.reset();                          // ⚠️ clean stale state before reuse
    this.available.push(conn);             // back in the pool for the next caller
  }
}

// Client code: acquire → use → release.
const pool = new ConnectionPool(3);
const c = pool.acquire();                  // borrow one
c?.query("SELECT 1");                      // use it
if (c) pool.release(c);                    // ALWAYS give it back
// Serve 1000 requests through this pool and only 3 connections are ever created.

References & further reading

6 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 05

What problem does the Object Pool pattern primarily solve?

question 02 / 05

What is the correct lifecycle of a pooled object?

question 03 / 05

You serve 5,000 requests through a pool with a maximum size of 3. Roughly how many expensive objects get created?

question 04 / 05

All connections in a fixed-size pool are in use and another request arrives. What is the most common thing a pool does?

question 05 / 05

Which is a real failure mode specific to using an Object Pool?

0/5 answered