Object Pool
Intent
Pre-allocate and reuse a fixed set of expensive objects rather than creating and destroying them on demand, improving performance by avoiding costly initialization and garbage collection overhead.
Problem
Creating certain objects is expensive due to resource allocation costs like database connections, thread creation, or large memory buffers. Frequently allocating and deallocating such objects causes performance degradation from initialization overhead and increased garbage collection pressure. Some resources have hard limits (connection pools, hardware handles) that prevent unlimited creation. Uncontrolled object creation can exhaust system resources.
Real-World Analogy
Think of a car rental agency at an airport. Instead of manufacturing a new car every time someone needs one and scrapping it when they’re done, the agency maintains a fleet of ready-to-drive cars. When you arrive, you check out an available car from the lot. If all cars are rented out, you wait or get referred elsewhere. When your trip is over, you return the car, it gets cleaned up, and it’s ready for the next customer. The agency might keep 50 cars — enough to handle normal demand without wasting money on thousands of cars that would sit idle in the lot.
When You Need It
- Managing expensive resources like database connections, thread pools, or network sockets
- Optimizing performance in systems with frequent allocation/deallocation of large or costly objects
- Working with resources that have hard limits or require explicit lifecycle management
UML Class Diagram
classDiagram
class ObjectPool~T~ {
-available: Queue~T~
-inUse: Set~T~
-maxSize: int
+acquire() T
+release(obj: T)
+create() T
+validate(obj: T) bool
+reset(obj: T)
}
class PooledObject {
<<interface>>
+reset()
+isValid() bool
}
class Client {
+doWork()
}
class ConcretePooledObject {
-state: State
+reset()
+isValid() bool
+doOperation()
}
ObjectPool "1" --> "*" PooledObject : manages
PooledObject <|.. ConcretePooledObject
Client ..> ObjectPool : acquires/releases
Client ..> PooledObject : uses
Sequence Diagram
sequenceDiagram
participant Client1
participant Pool
participant Object1
participant Client2
participant Object2
participant Client3
Client1->>Pool: Request object
Pool->>Client1: Return Object1
Client2->>Pool: Request object
Pool->>Client2: Return Object2
Client1->>Pool: Release Object1
Client3->>Pool: Request object
Pool->>Client3: Return Object1 (reused)
Participants
- ObjectPool — manages the lifecycle of pooled objects, tracking available and in-use instances
- PooledObject — interface defining reset() and validation methods for objects in the pool
- ConcretePooledObject — the actual expensive resource being pooled (connection, buffer, etc.)
- Client — acquires objects from the pool, uses them, and releases them back
How It Works
- The ObjectPool is initialized with a set of pre-created instances or a lazy creation strategy up to maxSize
- When a Client needs an object, it calls acquire() which removes an available object from the pool or creates a new one if under capacity
- The Client uses the pooled object to perform work, treating it like any other instance
- When finished, the Client calls release() to return the object to the pool rather than letting it be garbage collected
- Before returning an object to the available queue, the pool validates it and resets its state to prevent leaking data between uses
Applicability
Use when:
- Object creation/destruction is measurably expensive in terms of time or resources
- You have a predictable maximum number of concurrent object users and can set appropriate pool size
- Objects can be reset to a clean state and safely reused across different contexts
Don’t use when:
- Objects are cheap to create and garbage collection overhead is negligible
- Resource usage is unbounded and you can’t determine a reasonable pool size
- Objects cannot be safely reset or have complex state that leaks between uses
Trade-offs
Pros:
- Dramatically reduces allocation overhead and garbage collection pressure for expensive objects
- Provides bounded resource usage with configurable pool size limits
- Improves predictable performance by amortizing initialization costs across many uses
Cons:
- Adds complexity with acquire/release lifecycle that clients must manage correctly
- Leaked objects (not released) can exhaust the pool and cause deadlocks or resource starvation
- Memory footprint is higher since objects remain allocated even when not in use
Example Code
C#
using System;
using System.Collections.Concurrent;
using System.Threading;
// Expensive resource to be pooled
class DatabaseConnection
{
public int Id { get; }
private static int _nextId = 1;
public DatabaseConnection()
{
Id = _nextId++;
Console.WriteLine($" [Created new connection #{Id}]");
Thread.Sleep(100); // Simulate expensive initialization
}
public void ExecuteQuery(string query)
{
Console.WriteLine($" Connection #{Id} executing: {query}");
}
public void Reset()
{
Console.WriteLine($" Connection #{Id} reset");
}
}
// Object Pool implementation
class ObjectPool<T> where T : new()
{
private readonly ConcurrentBag<T> _available = new();
private int _totalCount = 0;
private readonly int _maxSize;
public ObjectPool(int maxSize)
{
_maxSize = maxSize;
}
public T Acquire()
{
if (_available.TryTake(out T item))
{
Console.WriteLine($"Reusing object from pool");
return item;
}
if (_totalCount < _maxSize)
{
Interlocked.Increment(ref _totalCount);
Console.WriteLine($"Creating new object (pool size: {_totalCount}/{_maxSize})");
return new T();
}
throw new InvalidOperationException("Pool exhausted!");
}
public void Release(T item)
{
if (item is DatabaseConnection conn)
{
conn.Reset();
}
_available.Add(item);
Console.WriteLine($"Released object back to pool (available: {_available.Count})");
}
public void PrintStats()
{
Console.WriteLine($"Pool stats - Total: {_totalCount}, Available: {_available.Count}, In use: {_totalCount - _available.Count}");
}
}
class Program
{
static void Main()
{
var pool = new ObjectPool<DatabaseConnection>(maxSize: 3);
Console.WriteLine("=== Acquiring connections ===");
var conn1 = pool.Acquire();
conn1.ExecuteQuery("SELECT * FROM users");
var conn2 = pool.Acquire();
conn2.ExecuteQuery("INSERT INTO logs...");
Console.WriteLine("\n=== Releasing and reusing ===");
pool.Release(conn1);
pool.PrintStats();
var conn3 = pool.Acquire(); // Should reuse conn1
conn3.ExecuteQuery("UPDATE products...");
pool.Release(conn2);
pool.Release(conn3);
Console.WriteLine("\n=== Final stats ===");
pool.PrintStats();
}
}
Runnable Examples
| Language | File |
|---|---|
| C# | object-pool.cs |
Related Patterns
- Singleton — pools are often implemented as singletons to provide global access
- Factory Method — pools use factory methods to create new instances when needed
- Flyweight — both share objects for efficiency, but pools focus on expensive mutable resources while flyweights share immutable state