Skip to content

Scaling

Scaling TypeORM Applications for High Traffic

Section titled “Scaling TypeORM Applications for High Traffic”

Scaling ensures your application can handle increased load while maintaining performance.

Scaling Dimensions
================================================================================
Vertical Scaling (Scale Up):
+------------------+ +------------------+
| Server | --> | Bigger Server |
| 4 CPU, 8GB | | 16 CPU, 64GB |
+------------------+ +------------------+
Horizontal Scaling (Scale Out):
+------------------+ +------------------+ +------------------+
| Server 1 | | Server 2 | | Server 3 |
| 4 CPU, 8GB | | 4 CPU, 8GB | | 4 CPU, 8GB |
+------------------+ +------------------+ +------------------+
\ | /
\ | /
+------------------------------------------+
| Load Balancer |
+------------------------------------------+
Database Scaling:
- Read Replicas
- Connection Pooling
- Sharding
- Caching
================================================================================

// BAD: In-memory state (doesn't scale)
@Injectable()
export class UsersService {
private sessionCache = new Map<string, User>(); // Local state
async setSession(sessionId: string, user: User) {
this.sessionCache.set(sessionId, user); // Lost on restart/scale
}
}
// GOOD: External state (scales horizontally)
@Injectable()
export class UsersService {
constructor(
@Inject(CACHE_MANAGER)
private cacheManager: Cache, // Redis-backed cache
) {}
async setSession(sessionId: string, user: User) {
await this.cacheManager.set(`session:${sessionId}`, user, 3600000);
}
}
# nginx.conf for load balancing
upstream backend {
least_conn; # Route to least connected server
server backend1:3000 weight=3;
server backend2:3000 weight=2;
server backend3:3000 weight=1;
keepalive 32;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
// Write replica (primary)
replication: {
master: {
host: configService.get('DB_PRIMARY_HOST'),
port: configService.get('DB_PORT', 5432),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
},
slaves: [
{
host: configService.get('DB_REPLICA1_HOST'),
port: configService.get('DB_PORT', 5432),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
},
{
host: configService.get('DB_REPLICA2_HOST'),
port: configService.get('DB_PORT', 5432),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
},
],
},
poolSize: 20,
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: false,
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
// TypeORM automatically routes:
// - SELECT queries to replicas
// - INSERT/UPDATE/DELETE to master
src/database/read-write.service.ts
import { Injectable, InjectDataSource } from '@nestjs/common';
import { DataSource } from 'typeorm';
@Injectable()
export class ReadWriteService {
constructor(
@InjectDataSource('write')
private writeDataSource: DataSource,
@InjectDataSource('read')
private readDataSource: DataSource,
) {}
// Use for reads
get readManager() {
return this.readDataSource.manager;
}
// Use for writes
get writeManager() {
return this.writeDataSource.manager;
}
// Execute read query
async read<T>(query: (manager: any) => Promise<T>): Promise<T> {
return query(this.readManager);
}
// Execute write query
async write<T>(query: (manager: any) => Promise<T>): Promise<T> {
return query(this.writeManager);
}
}
// Usage
@Injectable()
export class UsersService {
constructor(private readWrite: ReadWriteService) {}
async findAll(): Promise<User[]> {
return this.readWrite.read(manager =>
manager.find(User)
);
}
async create(dto: CreateUserDto): Promise<User> {
return this.readWrite.write(manager =>
manager.save(User, dto)
);
}
}

src/database/pool-scaler.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
@Injectable()
export class PoolScalerService implements OnModuleInit, OnModuleDestroy {
private interval: NodeJS.Timeout;
private minPoolSize = 5;
private maxPoolSize = 50;
private targetUtilization = 0.7;
constructor(
@InjectDataSource()
private dataSource: DataSource,
) {}
onModuleInit() {
// Check every minute
this.interval = setInterval(() => this.adjustPoolSize(), 60000);
}
onModuleDestroy() {
clearInterval(this.interval);
}
private async adjustPoolSize() {
const stats = this.getPoolStats();
const utilization = stats.active / stats.total;
if (utilization > this.targetUtilization + 0.1) {
// Scale up
const newSize = Math.min(stats.total + 5, this.maxPoolSize);
await this.setPoolSize(newSize);
} else if (utilization < this.targetUtilization - 0.1) {
// Scale down
const newSize = Math.max(stats.total - 5, this.minPoolSize);
await this.setPoolSize(newSize);
}
}
private getPoolStats() {
const driver = this.dataSource.driver as any;
const pool = driver.pool;
return {
total: pool.totalCount,
idle: pool.idleCount,
active: pool.totalCount - pool.idleCount,
waiting: pool.waitingCount,
};
}
private async setPoolSize(size: number) {
// Note: This requires pool library support
const driver = this.dataSource.driver as any;
if (driver.pool?.options) {
driver.pool.options.max = size;
}
}
}

src/cache/distributed-cache.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
@Injectable()
export class DistributedCacheService {
constructor(
@Inject(CACHE_MANAGER)
private cacheManager: Cache,
) {}
async getOrSet<T>(
key: string,
factory: () => Promise<T>,
ttl: number = 60000,
): Promise<T> {
// Try cache
const cached = await this.cacheManager.get<T>(key);
if (cached !== undefined) {
return cached;
}
// Use distributed lock to prevent cache stampede
const lockKey = `lock:${key}`;
const locked = await this.acquireLock(lockKey, 5000);
if (!locked) {
// Wait and retry
await this.sleep(100);
return this.getOrSet(key, factory, ttl);
}
try {
// Double-check after acquiring lock
const cachedAfterLock = await this.cacheManager.get<T>(key);
if (cachedAfterLock !== undefined) {
return cachedAfterLock;
}
// Execute factory
const value = await factory();
// Store in cache
await this.cacheManager.set(key, value, ttl);
return value;
} finally {
await this.releaseLock(lockKey);
}
}
private async acquireLock(key: string, ttl: number): Promise<boolean> {
const store = this.cacheManager.store as any;
if (store.setnx) {
return store.setnx(key, '1', ttl);
}
return true; // Fallback if no distributed lock support
}
private async releaseLock(key: string): Promise<void> {
await this.cacheManager.del(key);
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

Database Sharding
================================================================================
Horizontal Partitioning by Key:
+------------------+ +------------------+ +------------------+
| Shard 1 | | Shard 2 | | Shard 3 |
| (users 1-1M) | | (users 1M-2M) | | (users 2M-3M) |
+------------------+ +------------------+ +------------------+
Sharding Key Selection:
- User ID (most common)
- Geographic region
- Tenant ID (multi-tenant)
- Date range (time-series)
================================================================================
src/database/sharding.service.ts
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
interface ShardConfig {
id: string;
host: string;
port: number;
rangeStart: number;
rangeEnd: number;
}
@Injectable()
export class ShardingService {
private shards: Map<string, DataSource> = new Map();
private shardConfigs: ShardConfig[] = [];
async initializeShards(configs: ShardConfig[]) {
this.shardConfigs = configs;
for (const config of configs) {
const dataSource = new DataSource({
type: 'postgres',
host: config.host,
port: config.port,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
entities: ['src/**/*.entity{.ts,.js}'],
poolSize: 10,
});
await dataSource.initialize();
this.shards.set(config.id, dataSource);
}
}
getShardByKey(key: number): DataSource {
for (const config of this.shardConfigs) {
if (key >= config.rangeStart && key < config.rangeEnd) {
return this.shards.get(config.id)!;
}
}
throw new Error(`No shard found for key: ${key}`);
}
getShardForUser(userId: number): DataSource {
return this.getShardByKey(userId);
}
async closeAll() {
for (const dataSource of this.shards.values()) {
if (dataSource.isInitialized) {
await dataSource.destroy();
}
}
this.shards.clear();
}
}
// Usage
@Injectable()
export class UsersService {
constructor(private sharding: ShardingService) {}
async findOne(userId: number): Promise<User> {
const shard = this.sharding.getShardForUser(userId);
return shard.manager.findOne(User, { where: { id: userId } });
}
async create(user: Partial<User>): Promise<User> {
const shard = this.sharding.getShardForUser(user.id);
return shard.manager.save(User, user);
}
}

k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myapp-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 100
periodSeconds: 15
- type: Pods
value: 4
periodSeconds: 15
selectPolicy: Max
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
src/monitoring/metrics.service.ts
import { Injectable } from '@nestjs/common';
import { PrometheusMetricsService } from './prometheus.service';
@Injectable()
export class CustomMetricsService {
constructor(private prometheus: PrometheusMetricsService) {}
// Track for autoscaling decisions
async recordRequestMetrics(duration: number, success: boolean) {
this.prometheus.recordRequest(duration, success);
}
async recordQueueLength(length: number) {
this.prometheus.recordQueueLength(length);
}
async recordActiveConnections(count: number) {
this.prometheus.recordConnections(count);
}
}

Scaling Checklist
================================================================================
Application:
[ ] Stateless design (no local state)
[ ] External session storage
[ ] Connection pooling configured
[ ] Caching implemented
[ ] Rate limiting enabled
Database:
[ ] Read replicas configured
[ ] Connection pool sized appropriately
[ ] Indexes optimized
[ ] Query performance monitored
[ ] Sharding strategy (if needed)
Infrastructure:
[ ] Load balancer configured
[ ] Auto-scaling enabled
[ ] Health checks implemented
[ ] Monitoring and alerting
[ ] Disaster recovery plan
Performance:
[ ] Response time targets defined
[ ] Throughput targets defined
[ ] Load testing completed
[ ] Bottlenecks identified and resolved
================================================================================

Scaling Quick Reference
+------------------------------------------------------------------+
| |
| Strategy | Use Case |
| -------------------|------------------------------------------|
| Vertical | Simple, limited scale |
| Horizontal | High availability, unlimited scale |
| Read Replicas | Read-heavy workloads |
| Sharding | Very large datasets |
| Caching | Reduce database load |
| |
| Components | Description |
| -------------------|------------------------------------------|
| Load Balancer | Distribute traffic |
| Connection Pool | Reuse database connections |
| Cache Layer | Store frequently accessed data |
| Auto-scaler | Automatic capacity adjustment |
| |
| Best Practices | Description |
| -------------------|------------------------------------------|
| Stateless | No local state in application |
| External state | Use Redis for sessions |
| Monitor | Track key metrics |
| Test at scale | Load test before production |
| |
+------------------------------------------------------------------+

Chapter 41: Unit Testing


Last Updated: February 2026