LESSON 4.7
Dependency injection and object pools
DI wires objects from the outside; object pools reuse expensive objects instead of creating them fresh. Both control construction and lifecycle costs.
15 min read
Real-world analogy
Imagine a commercial construction crew erecting a high-rise building. Rather than mining iron ore and forging a diesel crane on the job site every time heavy steel beams need lifting, the foreman orders an industrial crane from an equipment rental company. The crane arrives on-site fully assembled with a certified operator—this is Dependency Injection.
When the subcontractor finishes lifting concrete girders, they don't dismantle the crane or toss it into a landfill. They return it to the rental depot's central fleet yard so another contracting team working across town can lease and deploy it—this is an Object Pool.
The problem
When domain services create their own dependencies using hardcoded new invocations, two major failure modes emerge: tight coupling and runaway resource creation overhead.
- Tight Coupling: If an
OrderServiceconstructs a concretePostgresConnectionandSmtpClientdirectly inside its methods, you cannot swap the database provider, point queries to an in-memory replica, or supply mock fixtures during unit testing. - Resource Exhaustion: Creating a new TCP connection, performing TLS negotiation, authenticating credentials, and allocating kernel buffers takes 50 to 200 milliseconds per request. Under peak traffic of 5,000 requests per second, creating connections on demand exhausts socket file descriptors and crashes the database server.
The naive code below illustrates both traps:
class PostgresConnection {
connect() {
console.log("[Postgres] Heavy TCP handshake + TLS auth (120ms)");
}
query(sql) {
console.log(`[Postgres] Executing: ${sql}`);
}
}
class OrderService {
processOrder(orderId, amount) {
// Problem 1: Hardcoded construction couples to concrete Postgres
// Problem 2: Paying 120ms connection handshake on every single operation
const db = new PostgresConnection();
db.connect();
db.query(`INSERT INTO orders VALUES ('${orderId}', ${amount})`);
}
}
const service = new OrderService();
service.processOrder("ord-101", 99.95);public class Example {
static class PostgresConnection {
public void connect() {
System.out.println("[Postgres] Heavy TCP handshake + TLS auth (120ms)");
}
public void query(String sql) {
System.out.println("[Postgres] Executing: " + sql);
}
}
The solution
The Dependency Injection (DI) pattern separates object creation from object usage: classes receive their required collaborators through constructor parameters from an external composition root. The Object Pool pattern maintains an active inventory of pre-initialized, reusable resources, lending them out via an explicit checkout/release protocol.
Participants
- Client / Consumer (
UserService): Declares required collaborator interfaces and uses pooled resources through try-finally blocks. - Resource Interface (
DatabaseConnection): The common contract exposed to consumers. - Object Pool (
ConnectionPool): Manages pre-allocated resources, tracking available vs borrowed instances and handling pool exhaustion. - Composition Root / Injector (
main): Wires the pool and services together at application startup.
Step-by-step implementation
Step 1: Constructor injection for testability
Define explicit dependencies in the consumer's constructor rather than creating them internally.
class UserService {
#pool;
#mailer;
constructor(pool, mailer) {
this.#pool = pool;
this.#mailer = mailer;
}
}public class Example
Step 2: Pool acquire and release tracking
Implement an internal collection separating available resources from those currently leased to callers.
Step 3: Enforcing release symmetry with try-finally
Ensure every acquired connection is returned to the pool, even if business logic throws an error.
The complete example
Here is a runnable application showing a multi-tier service configured via pure Dependency Injection, querying through a capped Connection Pool that reuses connections across operations.
Initial pool stats: { available: 2, inUse: 0, total: 2 }
[conn-2] Executed: INSERT INTO users VALUES ('alice')
[Email to alice@example.com] Welcome alice!
[conn-2] Executed: INSERT INTO users VALUES ('bob')
[Email to bob@example.com] Welcome bob!
Final pool stats: { available: 2, inUse: 0, total: 2 }Variations
Constructor injection vs field and setter injection
- Constructor Injection: Mandates that all dependencies are supplied at instantiation time. Enables declaring fields
readonly(TypeScript) orfinal(Java). Guarantees that instances are always fully initialized and valid. - Setter Injection: Dependencies are provided via public
setService()methods after instantiation. Useful for circular dependencies or optional plugins, but leaves the object vulnerable to temporal coupling where methods fail if called before setters run. - Field Injection (Reflection /
@Autowired): Framework annotations placed directly on private fields. While concise, it hides dependencies from callers, prevents instantiation outside a framework container in standard unit tests, and violates encapsulation.
Container vs manual DI (Pure DI)
- Pure (Manual) DI: Wiring the entire dependency graph explicitly by hand in the application's
main()or entrypoint file. No reflection, zero framework overhead, complete compile-time type safety, and crystal-clear stack traces. Highly recommended for small-to-medium systems. - IoC Containers (Spring, InversifyJS, NestJS): The framework scans annotations, inspects types via reflection, builds an internal directed graph, and resolves instances automatically. Essential in massive enterprise systems with thousands of components, but adds reflection overhead, complex lifecycle hooks, and runtime resolution bugs.
Object pool sizing and leak detection
- Pool Sizing Formula: Sizing a connection pool follows Little's Law and hardware limits:
Pool Size = Connections Per Core * Core Count + Disk Spindle / IOPS Allowance. Setting a pool to 500 connections when the database server has 8 cores degrades performance through context-switching and lock contention. - Leak Detection Thresholds: Production pools like HikariCP track lease timestamps. If a caller holds a connection longer than
leakDetectionThreshold(e.g. 5,000ms), the pool logs an exception containing the exact stack trace whereacquire()was called to expose forgotten releases.
Where you've already used it
- Spring Framework (
ApplicationContext): Automatically creates singletons, resolves constructor parameters, and injects beans into dependent services. - HikariCP & Apache Commons DBCP: High-performance JDBC connection pools in Java that manage pre-allocated physical socket connections to PostgreSQL and MySQL.
- Node.js
pg-pool&mysql2/promise: Manages client checkout and recycling for asynchronous database operations in Node. - Thread Pools (
java.util.concurrent.Executors):newFixedThreadPool(16)recycles an allocated set of OS threads rather than spawning new threads per task.
Pros and cons
Advantages
- Loose Coupling: Classes depend on abstractions and contracts rather than hardcoded concrete types.
- Testability: Any external dependency (database, payment gateway, clock) can be substituted with a mock or stub in tests.
- Resource Amortization: Heavy construction costs (sockets, threads, memory buffers) are paid once at startup rather than on every user request.
- Backpressure & Protection: A capped pool acts as a bulkhead, preventing traffic spikes from overwhelming downstream databases.
Disadvantages
- Resource Leak Hazard: Forgetting a
finally { pool.release(conn) }permanently leaks pool capacity. - Stale State: Reused objects can retain dirty state (uncommitted transactions, open cursors, or residual user data) across borrowers.
- Indirection Overhead: Pure DI requires passing dependencies through multiple architectural layers or managing an IoC container.
When not to use it
Do not use an Object Pool for lightweight, stateless objects (e.g. DTOs, string formatters, or small calculation strategies). Modern V8 and JVM garbage collectors can allocate and collect short-lived ephemeral objects in nanoseconds. Pooling lightweight objects introduces synchronization overhead and memory bloat without performance benefit.
Related patterns
| Pattern | Focus | Distinction |
|---|---|---|
| Dependency Injection | Inverting collaborator ownership | Supplies collaborators from outside rather than building them |
| Object Pool | Reusing expensive resources | Recycles a fixed inventory of pre-allocated instances |
| Factory Method | Encapsulating object creation | Creates new instances on demand rather than recycling existing ones |
| Singleton | Single global instance | A pool provides multiple reusable instances with checked-out states |
| Flyweight | Sharing immutable fine-grained state | Flyweights are shared simultaneously; pooled objects are leased exclusively |
Interview questions
Why is constructor injection universally preferred over field injection? Constructor injection enforces immutability, guarantees that an object cannot be constructed in an incomplete or invalid state, and makes dependencies explicit without requiring reflection or a container during testing.
How does an Object Pool differ from a Flyweight pattern? A Flyweight is shared simultaneously by multiple clients and must be strictly immutable. A Pooled object is leased exclusively to one client at a time, may hold mutable state during the lease, and must be sanitized upon release.
What is the "temporal coupling" issue associated with setter injection? Setter injection allows an object to exist in memory before its required dependencies are provided. If a business method is called between the constructor and the setter, it fails with a null reference exception at runtime.
How does a connection pool handle a scenario where all connections are in use?
Depending on its configured policy, the pool will either: (1) block the calling thread with a timeout until a connection is released, (2) throw an immediate PoolExhaustedException (fail-fast), or (3) dynamically spawn a temporary connection up to a strict maximum limit.
Try it
Connection Pool LLD
Manage bounded resources, acquisition deadlines and safe lease return under concurrency.