0

Object Pool — reusing expensive objects

Advanced5 min read·eng-02-006
performancecompare

Concept

Object Pool pattern maintains a pool of pre-initialized, reusable objects. Instead of creating and destroying objects repeatedly, objects are "checked out" from the pool, used, then returned for reuse.

When to use Object Pool:

  • Object creation is expensive: database connections, network sockets, thread/process handles, large buffers.
  • Objects can be reset to a clean state after use.
  • High-frequency creation/destruction causes performance issues (GC pressure, connection overhead).
  • Database connection pools are the canonical example — creating a new DB connection takes 50-100ms. A pool keeps N connections ready.

Pool mechanics:

  1. Pre-create a set of objects (or create lazily on first demand).
  2. On acquire(): return an available object, or wait/create more if pool is empty.
  3. On release(): reset the object and return it to the pool.

PHP context: PHP is typically request-scoped — the process (and all objects) die at request end. A traditional in-process object pool has limited value since the process restarts. However:

  • PDO connection pools: Not native in PHP, but pg_pconnect() and PDO::ATTR_PERSISTENT provide persistent connections via FastCGI.
  • Laravel Octane/Swoole: Long-running PHP processes where object pools make sense.
  • External pools: Connection poolers like PgBouncer (PostgreSQL) or ProxySQL (MySQL) live outside PHP.

SplQueue for implementation: PHP's built-in SplQueue provides efficient FIFO access for pool management.

Code Example

php
<?php
// Simple object pool implementation
interface Poolable
{
    public function reset(): void; // called when returning to pool
    public function isHealthy(): bool; // can the pool reuse this object?
}

class DatabaseConnection implements Poolable
{
    private ?\PDO $pdo = null;

    public function __construct(private readonly string $dsn, private readonly string $user, private readonly string $pass) {}

    public function connect(): void
    {
        $this->pdo = new \PDO($this->dsn, $this->user, $this->pass, [
            \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
        ]);
    }

    public function query(string $sql, array $bindings = []): array
    {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($bindings);
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }

    public function reset(): void
    {
        // For DB connections, ensure no open transactions
        if ($this->pdo->inTransaction()) {
            $this->pdo->rollBack();
        }
    }

    public function isHealthy(): bool
    {
        try {
            $this->pdo?->query('SELECT 1');
            return true;
        } catch (\PDOException) {
            return false;
        }
    }
}

class ObjectPool
{
    private \SplQueue $available;
    private array     $inUse = [];
    private int       $size  = 0;

    public function __construct(
        private readonly int      $maxSize,
        private readonly callable $factory,
    ) {
        $this->available = new \SplQueue();
    }

    public function acquire(): mixed
    {
        if (!$this->available->isEmpty()) {
            $obj = $this->available->dequeue();
            $this->inUse[spl_object_id($obj)] = $obj;
            return $obj;
        }

        if ($this->size < $this->maxSize) {
            $obj = ($this->factory)();
            $this->size++;
            $this->inUse[spl_object_id($obj)] = $obj;
            return $obj;
        }

        throw new \RuntimeException("Object pool exhausted (max: {$this->maxSize}).");
    }

    public function release(mixed $obj): void
    {
        $id = spl_object_id($obj);
        unset($this->inUse[$id]);

        if ($obj instanceof Poolable && $obj->isHealthy()) {
            $obj->reset();
            $this->available->enqueue($obj);
        } else {
            $this->size--; // discard unhealthy object
        }
    }

    public function availableCount(): int { return $this->available->count(); }
    public function inUseCount(): int     { return count($this->inUse); }
}

// Real-world note: for production PHP, use external connection poolers:
// PostgreSQL: PgBouncer (pgbouncer.org)
// MySQL: ProxySQL (proxysql.com)
// Laravel Octane + Swoole: built-in connection reuse

## Interview Q&A

**Q: Does Object Pool make sense in traditional PHP request/response PHP?**

A: Rarely. Traditional PHP (FPM) creates a new process per request — all objects die at request end, so pooling within a request provides little benefit. The exception is when you need multiple connections within a single request. External poolers (PgBouncer, ProxySQL) are more appropriate. Object Pool makes more sense with Swoole/Octane where PHP processes are long-lived and handle multiple requests.