System Design: Principles and Best Practices
System design is crucial for building scalable and reliable applications. Let's explore essential principles and patterns for designing robust systems.
System Architecture
1. Microservices Architecture
Design scalable microservices:
// services/user/src/types/index.ts interface User { id: string; email: string; name: string; createdAt: Date; } interface UserService { createUser(data: CreateUserDTO): Promise<User>; getUser(id: string): Promise<User>; updateUser(id: string, data: UpdateUserDTO): Promise<User>; deleteUser(id: string): Promise<void>; } // services/user/src/service.ts class UserServiceImpl implements UserService { constructor( private readonly repository: UserRepository, private readonly eventBus: EventBus, private readonly cache: CacheService ) {} async createUser(data: CreateUserDTO): Promise<User> { const user = await this.repository.create(data); await this.eventBus.publish("user.created", { userId: user.id, email: user.email, }); await this.cache.set(`user:${user.id}`, user); return user; } async getUser(id: string): Promise<User> { const cached = await this.cache.get<User>(`user:${id}`); if (cached) return cached; const user = await this.repository.findById(id); if (!user) throw new NotFoundError("User not found"); await this.cache.set(`user:${id}`, user); return user; } }
2. Event-Driven Architecture
Implement event-driven systems:
// lib/event-bus/index.ts interface Event<T = any> { id: string; type: string; data: T; metadata: { timestamp: Date; correlationId: string; }; } interface EventBus { publish<T>(type: string, data: T): Promise<void>; subscribe<T>(type: string, handler: (event: Event<T>) => Promise<void>): void; } // Implementation with Kafka class KafkaEventBus implements EventBus { constructor(private kafka: Kafka) {} async publish<T>(type: string, data: T): Promise<void> { const producer = this.kafka.producer(); await producer.connect(); await producer.send({ topic: type, messages: [ { key: uuidv4(), value: JSON.stringify({ id: uuidv4(), type, data, metadata: { timestamp: new Date(), correlationId: getCorrelationId(), }, }), }, ], }); await producer.disconnect(); } subscribe<T>( type: string, handler: (event: Event<T>) => Promise<void> ): void { const consumer = this.kafka.consumer({ groupId: `${type}-consumer`, }); consumer.connect(); consumer.subscribe({ topic: type }); consumer.run({ eachMessage: async ({ message }) => { const event = JSON.parse(message.value!.toString()); await handler(event); }, }); } }
Scalability Patterns
1. Caching Strategy
Implement distributed caching:
// lib/cache/index.ts interface CacheOptions { ttl?: number; namespace?: string; } interface CacheService { get<T>(key: string): Promise<T | null>; set<T>(key: string, value: T, options?: CacheOptions): Promise<void>; delete(key: string): Promise<void>; clear(namespace?: string): Promise<void>; } // Implementation with Redis class RedisCacheService implements CacheService { constructor(private redis: Redis, private options: CacheOptions = {}) {} private getKey(key: string): string { return this.options.namespace ? `${this.options.namespace}:${key}` : key; } async get<T>(key: string): Promise<T | null> { const value = await this.redis.get(this.getKey(key)); return value ? JSON.parse(value) : null; } async set<T>( key: string, value: T, options: CacheOptions = {} ): Promise<void> { const ttl = options.ttl || this.options.ttl; const fullKey = this.getKey(key); if (ttl) { await this.redis.setex(fullKey, ttl, JSON.stringify(value)); } else { await this.redis.set(fullKey, JSON.stringify(value)); } } async delete(key: string): Promise<void> { await this.redis.del(this.getKey(key)); } async clear(namespace?: string): Promise<void> { const pattern = namespace ? `${namespace}:*` : this.options.namespace ? `${this.options.namespace}:*` : "*"; const keys = await this.redis.keys(pattern); if (keys.length) { await this.redis.del(...keys); } } }
2. Load Balancing
Configure load balancing:
// infrastructure/load-balancer.ts interface LoadBalancer { getNextServer(): Server; addServer(server: Server): void; removeServer(server: Server): void; updateHealth(server: Server, isHealthy: boolean): void; } // Round Robin Implementation class RoundRobinLoadBalancer implements LoadBalancer { private servers: Server[] = []; private currentIndex = 0; getNextServer(): Server { if (!this.servers.length) { throw new Error("No servers available"); } const server = this.servers[this.currentIndex]; this.currentIndex = (this.currentIndex + 1) % this.servers.length; return server; } addServer(server: Server): void { this.servers.push(server); } removeServer(server: Server): void { this.servers = this.servers.filter((s) => s.id !== server.id); } updateHealth(server: Server, isHealthy: boolean): void { const index = this.servers.findIndex((s) => s.id === server.id); if (index !== -1) { this.servers[index].isHealthy = isHealthy; } } } // Weighted Round Robin Implementation class WeightedRoundRobinLoadBalancer implements LoadBalancer { private servers: WeightedServer[] = []; private currentIndex = 0; private currentWeight = 0; getNextServer(): Server { const server = this.getNextWeightedServer(); if (!server) { throw new Error("No servers available"); } return server; } private getNextWeightedServer(): WeightedServer | null { if (!this.servers.length) return null; while (true) { this.currentIndex = (this.currentIndex + 1) % this.servers.length; if (this.currentIndex === 0) { this.currentWeight = this.currentWeight - 1; if (this.currentWeight <= 0) { this.currentWeight = this.getMaxWeight(); if (this.currentWeight === 0) return null; } } const server = this.servers[this.currentIndex]; if (server.weight >= this.currentWeight && server.isHealthy) { return server; } } } private getMaxWeight(): number { return Math.max(...this.servers.map((server) => server.weight)); } }
High Availability
1. Circuit Breaker
Implement circuit breaker pattern:
// lib/circuit-breaker/index.ts enum CircuitState { CLOSED, OPEN, HALF_OPEN, } interface CircuitBreakerOptions { failureThreshold: number; resetTimeout: number; halfOpenTimeout: number; } class CircuitBreaker { private state: CircuitState = CircuitState.CLOSED; private failures = 0; private lastFailureTime?: Date; private readonly options: CircuitBreakerOptions; constructor(options: CircuitBreakerOptions) { this.options = options; } async execute<T>(command: () => Promise<T>): Promise<T> { if (this.isOpen()) { throw new Error("Circuit is open"); } try { const result = await command(); this.onSuccess(); return result; } catch (error) { this.onFailure(); throw error; } } private isOpen(): boolean { if (this.state === CircuitState.CLOSED) { return false; } if (this.state === CircuitState.OPEN) { const now = new Date(); if ( this.lastFailureTime && now.getTime() - this.lastFailureTime.getTime() >= this.options.resetTimeout ) { this.state = CircuitState.HALF_OPEN; return false; } return true; } return false; } private onSuccess(): void { this.failures = 0; this.state = CircuitState.CLOSED; } private onFailure(): void { this.failures++; this.lastFailureTime = new Date(); if ( this.failures >= this.options.failureThreshold || this.state === CircuitState.HALF_OPEN ) { this.state = CircuitState.OPEN; } } }
Data Management
1. Database Sharding
Implement database sharding:
// lib/database/sharding.ts interface ShardingStrategy { getShardId(key: string): string; getAllShardIds(): string[]; } // Hash-based Sharding class HashShardingStrategy implements ShardingStrategy { constructor(private readonly shardCount: number) {} getShardId(key: string): string { const hash = this.hash(key); const shardId = hash % this.shardCount; return `shard_${shardId}`; } getAllShardIds(): string[] { return Array.from({ length: this.shardCount }, (_, i) => `shard_${i}`); } private hash(key: string): number { let hash = 0; for (let i = 0; i < key.length; i++) { hash = (hash << 5) - hash + key.charCodeAt(i); hash = hash & hash; } return Math.abs(hash); } } // Range-based Sharding class RangeShardingStrategy implements ShardingStrategy { constructor(private readonly ranges: Map<string, [number, number]>) {} getShardId(key: string): string { const value = parseInt(key); for (const [shardId, [min, max]] of this.ranges) { if (value >= min && value <= max) { return shardId; } } throw new Error("No shard found for key"); } getAllShardIds(): string[] { return Array.from(this.ranges.keys()); } }
Best Practices
- Design for Scale: Plan for horizontal scaling
- Fault Tolerance: Implement resilience patterns
- Data Management: Use appropriate data storage solutions
- Performance: Optimize for performance
- Monitoring: Implement comprehensive monitoring
- Security: Follow security best practices
- Documentation: Maintain system documentation
- Testing: Test for scalability and reliability
Implementation Checklist
- Define system requirements
- Design system architecture
- Implement scalability patterns
- Set up monitoring
- Configure high availability
- Implement data management
- Set up security measures
- Document system design
Conclusion
Effective system design is crucial for building scalable and reliable applications. Focus on implementing these patterns and practices to create robust systems.