Object Pools in Clojure

Use object pools sparingly in Clojure, mainly for expensive external resources rather than ordinary immutable values.

The object pool pattern keeps a reusable set of expensive resources so callers can borrow and return them instead of constructing new ones repeatedly. In Clojure, this pattern should be used much more narrowly than in older object-oriented systems.

That is because ordinary immutable values usually do not need pooling. Creating maps, vectors, sets, and small functions is usually not the kind of expensive resource problem that justifies a pool. Pools make more sense for external or heavyweight runtime resources such as:

  • database connections
  • HTTP client sessions
  • bounded worker resources
  • reusable native handles

The First Design Question

Before discussing implementation, ask one question:

Am I pooling expensive external resources, or am I trying to optimize ordinary value creation?

If the answer is “ordinary value creation,” the pool is probably the wrong tool. In modern JVM and Clojure code, object pools are often an outdated optimization unless profiling proves otherwise.

A Small Pool Model

When a pool is justified, the shape is usually simple: a queue of available resources plus clear borrow and return operations.

 1(ns app.pool)
 2
 3(defonce connection-pool
 4  (atom clojure.lang.PersistentQueue/EMPTY))
 5
 6(defn seed-pool! [connections]
 7  (reset! connection-pool (into clojure.lang.PersistentQueue/EMPTY connections)))
 8
 9(defn borrow-connection! []
10  (let [conn (peek @connection-pool)]
11    (when-not conn
12      (throw (ex-info "No connections available" {:type ::pool-empty})))
13    (swap! connection-pool pop)
14    conn))
15
16(defn return-connection! [conn]
17  (swap! connection-pool conj conn))

This example is intentionally small. In production, real pools usually need more:

  • validation of returned resources
  • timeouts
  • health checks
  • bounded size rules
  • cleanup on shutdown

That is why many teams use battle-tested pools from the underlying library ecosystem instead of building custom ones.

Why This Pattern Is Often Overused

Object pools look attractive because they promise performance. But in Clojure, a pool can easily create more complexity than value:

  • it introduces shared mutable coordination
  • it adds lifecycle and failure handling rules
  • it can create starvation or leaked-resource bugs
  • it may optimize the wrong thing

If the cost is really in network latency, disk access, or remote service time, pooling ordinary values will not help.

Better Defaults In Clojure

Prefer these defaults before building a custom pool:

  • use normal immutable values freely
  • reuse library-provided connection pools where appropriate
  • measure before optimizing
  • keep custom pooling at the boundary around genuinely expensive resources

This is one of the clearest examples of a classic GoF-style label that survives in Clojure only for a narrow subset of real problems.

Agents, Queues, And Alternatives

The older Clojure discussion of pools sometimes leans on agents and queues. A queue is still a natural way to model available resources. An agent can coordinate asynchronous actions, but it is not automatically the best pool primitive.

Often an atom plus a queue is enough for a small controlled design. For more involved pooling, the better answer is frequently:

  • a library-managed pool
  • a bounded executor
  • an explicit resource manager with health checks

Choose the simplest mechanism that matches the actual lifecycle.

Design Review Questions

Ask these before accepting an object-pool design:

  • What exact resource is expensive enough to pool?
  • Has profiling shown that creation cost matters?
  • Is a library pool already available?
  • Who validates, returns, and cleans up resources?

If those answers are not strong, the pool is probably unnecessary complexity.

Loading quiz…
Revised on Thursday, April 23, 2026