Skip to main content

Object Pooling

Learn how UltraFastBuilder uses object pooling to achieve exceptional performance and minimize garbage collection pressure.

Overview​

Object pooling is the secret behind UltraFastBuilder's high performance. Instead of creating new builder instances for every operation, we reuse existing instances from a pool.

  • Performance Gain: ~70% improvement over non-pooled builders
  • Memory: Reduces allocations by 95%+
  • GC Impact: Minimal garbage collection pressure

How It Works​

The Problem​

Without pooling:

// Every call creates new objects
for (let i = 0; i < 100000; i++) {
const builder = new UserBuilder(); // New allocation
const user = builder.withName('John').build();
// Builder becomes garbage
}
// Result: 100,000 allocations, GC runs frequently

The Solution​

With pooling:

// Reuse builders from a pool
const pool = new BuilderPool<UserBuilder>(() => new UserBuilder());

for (let i = 0; i < 100000; i++) {
const builder = pool.get(); // Reuse from pool
builder.reset(); // Clear previous data
const user = builder.withName('John').build();
pool.release(builder); // Return to pool
}
// Result: ~100 allocations, minimal GC

Automatic Pooling​

UltraFastBuilder automatically pools all builders:

import builder from '@ultra-fast-builder/core';

const createUser = builder(UserSchema);

// Behind the scenes:
// 1. Pool is created for this builder type
// 2. Each createUser() call gets a builder from the pool
// 3. After build(), builder is returned to pool
// 4. You don't manage any of this!

for (let i = 0; i < 100000; i++) {
const user = createUser().withName('John').build();
// Builder automatically returned to pool
}

Pool Architecture​

Pool Structure​

Each builder type gets its own pool:

// Internal structure (simplified)
const builderPools = new Map<string, BuilderPool>();

// Pool key: "zod-name,email,age"
builderPools.set('zod-name,email,age', new BuilderPool());

// Pool key: "class-Product"
builderPools.set('class-Product', new BuilderPool());

// Pool key: "interface-id,name"
builderPools.set('interface-id,name', new BuilderPool());

Pool Lifecycle​

1. createUser() called
↓
2. Check pool for available builder
↓
3a. Pool has builder → Reuse (HIT)
3b. Pool empty → Create new (MISS)
↓
4. Reset builder data
↓
5. Return builder instance to user
↓
6. User calls .build()
↓
7. Builder returned to pool automatically

Pool Statistics​

Viewing Pool Stats​

import { getPoolStats } from '@ultra-fast-builder/core';

// Run some operations
for (let i = 0; i < 10000; i++) {
createUser().withName('John').build();
}

// Check pool performance
const stats = getPoolStats();
console.log(stats);
// {
// totalPools: 1,
// totalObjects: 50,
// totalHits: 9950,
// totalMisses: 50,
// averageHitRate: 0.995 // 99.5% hit rate
// }

Understanding Stats​

  • totalPools: Number of active pools
  • totalObjects: Total pooled objects across all pools
  • totalHits: Number of times a pooled object was reused
  • totalMisses: Number of times a new object was created
  • averageHitRate: Percentage of requests served from pool (higher is better)

Hit Rate Interpretation​

  • 95%+: Excellent - pool is well-sized
  • 80-95%: Good - pool is working effectively
  • 50-80%: Fair - consider warming up the pool
  • Less than 50%: Poor - pool might be too small

Pool Configuration​

Default Configuration​

// Default pool settings (internal)
const DEFAULT_POOL_SIZE = 1000;

// Pool automatically grows/shrinks based on usage

Pool Warmup​

For consistent performance, warm up pools during startup:

import builder from '@ultra-fast-builder/core';

const createUser = builder(UserSchema);

// Warmup: Fill the pool
for (let i = 0; i < 100; i++) {
createUser().withName('warmup').build();
}

// Now the pool is ready for production traffic
console.log(getPoolStats());
// { totalObjects: 100, averageHitRate: 0 }

// After real traffic
for (let i = 0; i < 10000; i++) {
createUser().withName('John').build();
}

console.log(getPoolStats());
// { totalObjects: 100, averageHitRate: 0.99 } // 99% hit rate!

Memory Management​

Clearing Pools​

Clear pools to release memory:

import { clearPools } from '@ultra-fast-builder/core';

// During app lifecycle
clearPools(); // Releases all pooled objects

console.log(getPoolStats());
// { totalPools: 0, totalObjects: 0, totalHits: 0, totalMisses: 0 }

When to Clear Pools​

// ✅ GOOD: Clear during tests
afterEach(() => {
clearPools();
});

// ✅ GOOD: Clear during graceful shutdown
process.on('SIGTERM', () => {
clearPools();
server.close();
});

// ✅ GOOD: Clear after batch processing
async function processBatch(items: any[]) {
for (const item of items) {
await processItem(item);
}
clearPools(); // Free memory after batch
}

// ❌ BAD: Clearing during normal operation
app.post('/users', (req, res) => {
const user = createUser().build();
clearPools(); // DON'T DO THIS - defeats the purpose!
res.json(user);
});

Resetting Stats​

import { resetPoolStats } from '@ultra-fast-builder/core';

// Reset hit/miss counters without clearing pools
resetPoolStats();

console.log(getPoolStats());
// { totalObjects: 100, totalHits: 0, totalMisses: 0 }
// Objects still in pool, but counters reset

Performance Impact​

Benchmark: With vs Without Pooling​

// Without pooling (hypothetical)
console.time('no-pool');
for (let i = 0; i < 100000; i++) {
const builder = new UserBuilder(); // New allocation every time
builder.withName('John').build();
}
console.timeEnd('no-pool');
// no-pool: ~850ms

// With pooling (UltraFastBuilder)
console.time('with-pool');
for (let i = 0; i < 100000; i++) {
createUser().withName('John').build();
}
console.timeEnd('with-pool');
// with-pool: ~250ms (~70% faster!)

Memory Footprint​

// Monitor memory usage
const before = process.memoryUsage().heapUsed;

for (let i = 0; i < 100000; i++) {
createUser().withName('John').build();
}

const after = process.memoryUsage().heapUsed;
const increase = (after - before) / 1024 / 1024;

console.log(`Memory increase: ${increase.toFixed(2)} MB`);
// Memory increase: ~6 MB (vs ~60 MB without pooling)

Advanced Patterns​

Per-Request Pools​

For multi-tenant applications:

// DON'T: UltraFastBuilder uses global pools
// This pattern is just to illustrate the concept

class TenantBuilderPool {
private pools = new Map<string, any>();

getBuilder(tenantId: string) {
if (!this.pools.has(tenantId)) {
this.pools.set(tenantId, builder(UserSchema));
}
return this.pools.get(tenantId)!;
}

clearTenant(tenantId: string) {
this.pools.delete(tenantId);
}
}

Monitoring Pool Health​

function monitorPools() {
setInterval(() => {
const stats = getPoolStats();

if (stats.averageHitRate < 0.8) {
console.warn('⚠️ Low pool hit rate:', stats.averageHitRate);
console.warn('Consider warming up pools or increasing pool size');
}

if (stats.totalObjects > 5000) {
console.warn('⚠️ Large number of pooled objects:', stats.totalObjects);
console.warn('Consider calling clearPools() to free memory');
}

console.log('Pool stats:', {
hitRate: `${(stats.averageHitRate * 100).toFixed(1)}%`,
objects: stats.totalObjects,
pools: stats.totalPools,
});
}, 60000); // Every minute
}

// Start monitoring
monitorPools();

Pool Testing​

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { builder, getPoolStats, clearPools, resetPoolStats } from '@ultra-fast-builder/core';

describe('Pool performance', () => {
beforeEach(() => {
clearPools();
resetPoolStats();
});

afterEach(() => {
clearPools();
});

it('should achieve > 95% hit rate after warmup', () => {
const createUser = builder(UserSchema);

// Warmup
for (let i = 0; i < 100; i++) {
createUser().withName('warmup').build();
}

resetPoolStats();

// Test
for (let i = 0; i < 10000; i++) {
createUser().withName('test').build();
}

const stats = getPoolStats();
expect(stats.averageHitRate).toBeGreaterThan(0.95);
});

it('should not leak memory', () => {
const createUser = builder(UserSchema);

// Build many objects
for (let i = 0; i < 100000; i++) {
createUser().withName('test').build();
}

const stats = getPoolStats();
// Pool should stabilize, not grow indefinitely
expect(stats.totalObjects).toBeLessThan(200);
});
});

Best Practices​

1. Warm Up Pools in Production​

// app.ts
async function startup() {
console.log('Warming up builder pools...');

const createUser = builder(UserSchema);
const createOrder = builder(OrderSchema);

for (let i = 0; i < 100; i++) {
createUser().withName('warmup').withEmail('warmup@example.com').build();
createOrder().withId('warmup').withTotal(0).build();
}

const stats = getPoolStats();
console.log(`Pools warmed up: ${stats.totalObjects} objects ready`);

app.listen(3000);
}

startup();

2. Clear Pools During Tests​

import { afterEach } from 'vitest';
import { clearPools } from '@ultra-fast-builder/core';

// Global test setup
afterEach(() => {
clearPools(); // Prevent test pollution
});

3. Monitor in Production​

// Add to your monitoring/observability
app.get('/health', (req, res) => {
const stats = getPoolStats();

res.json({
status: 'ok',
pools: {
count: stats.totalPools,
objects: stats.totalObjects,
hitRate: `${(stats.averageHitRate * 100).toFixed(1)}%`,
},
});
});

Common Pitfalls​

❌ Clearing Pools Too Often​

// BAD: Defeats pooling
app.post('/users', (req, res) => {
const user = createUser().build();
clearPools(); // DON'T DO THIS
res.json(user);
});

❌ Creating New Factories​

// BAD: Creates new pool every time
function processUser(data: any) {
const createUser = builder(UserSchema); // New pool!
return createUser().withName(data.name).build();
}

// GOOD: Reuse factory
const createUser = builder(UserSchema);

function processUser(data: any) {
return createUser().withName(data.name).build();
}

❌ Not Warming Up for Benchmarks​

// BAD: Benchmark includes pool initialization
console.time('benchmark');
for (let i = 0; i < 100000; i++) {
createUser().withName('test').build();
}
console.timeEnd('benchmark');

// GOOD: Warmup before benchmark
for (let i = 0; i < 100; i++) {
createUser().withName('warmup').build();
}

console.time('benchmark');
for (let i = 0; i < 100000; i++) {
createUser().withName('test').build();
}
console.timeEnd('benchmark');

Next Steps​