Scalability & Clean Code
Code readability principles and system-level scalability patterns.
SOLID Principles
Single Responsibility Principle
// ❌ Class doing too much
class UserService {
async createUser(data: UserInput) { /* validation + DB + email + logging */ }
}
// ✅ Separated responsibilities
class UserValidator { validate(data: UserInput): ValidationResult { /* ... */ } }
class UserRepository { create(data: ValidUser): Promise<User> { /* ... */ } }
class WelcomeEmailService { send(user: User): Promise<void> { /* ... */ } }
class UserService {
constructor(
private validator: UserValidator,
private repo: UserRepository,
private emailService: WelcomeEmailService,
) {}
async createUser(data: UserInput) {
const valid = this.validator.validate(data)
const user = await this.repo.create(valid)
await this.emailService.send(user)
return user
}
}
Open/Closed Principle
// ❌ Modifying existing code for every new payment method
function processPayment(method: string, amount: number) {
if (method === "stripe") { /* ... */ }
else if (method === "paypal") { /* ... */ }
// adding "crypto" means editing this function again
}
// ✅ Open for extension, closed for modification
interface PaymentProcessor {
process(amount: number): Promise<PaymentResult>
}
class StripeProcessor implements PaymentProcessor { /* ... */ }
class PayPalProcessor implements PaymentProcessor { /* ... */ }
class CryptoProcessor implements PaymentProcessor { /* ... */ } // new — no edits elsewhere
class PaymentService {
constructor(private processor: PaymentProcessor) {}
process(amount: number) { return this.processor.process(amount) }
}
Dependency Inversion
// ❌ High-level module depends on low-level concrete implementation
class OrderService {
private db = new PostgresDatabase() // tightly coupled
}
// ✅ Depend on abstractions
interface Database {
query<T>(sql: string, params: unknown[]): Promise<T[]>
}
class OrderService {
constructor(private db: Database) {} // inject any implementation
}
// Easy to swap implementations or mock for tests
const orderService = new OrderService(new PostgresDatabase())
const testService = new OrderService(new InMemoryDatabase())
DRY — Don't Repeat Yourself (But Don't Over-Abstract)
// ❌ Repeated validation logic
function createUser(data: any) {
if (!data.email || !data.email.includes("@")) throw new Error("Invalid email")
// ...
}
function updateUser(data: any) {
if (!data.email || !data.email.includes("@")) throw new Error("Invalid email")
// ...
}
// ✅ Extract shared logic
const emailSchema = z.string().email()
function createUser(data: unknown) {
const validated = userCreateSchema.parse(data)
// ...
}
// ⚠️ AVOID premature abstraction — the "Rule of Three"
// Don't extract a shared function until you see the SAME logic 3+ times.
// Two similar-looking pieces of code might still be conceptually different.
Code Smells & Refactoring
Common Smells
| Smell |
Symptom |
Fix |
| Long function |
>30-50 lines, multiple responsibilities |
Extract smaller functions |
| Long parameter list |
>3-4 params |
Use an options object / DTO |
| Deep nesting |
>3 levels of if/for |
Early returns, extract conditions |
| God object |
One class does everything |
Split by responsibility (SRP) |
| Shotgun surgery |
One change requires edits in 10 files |
Consolidate related logic |
| Feature envy |
Method uses another object's data more than its own |
Move method to that object |
| Primitive obsession |
Passing raw strings/numbers everywhere |
Use value objects/branded types |
Refactoring: Extract & Early Return
// ❌ Deep nesting
function processOrder(order: Order) {
if (order) {
if (order.items.length > 0) {
if (order.status === "pending") {
if (order.user.isVerified) {
// actual logic buried 4 levels deep
}
}
}
}
}
// ✅ Guard clauses (early returns)
function processOrder(order: Order) {
if (!order) return
if (order.items.length === 0) return
if (order.status !== "pending") return
if (!order.user.isVerified) return
// actual logic at top level — much more readable
}
Branded Types (Fix Primitive Obsession)
// ❌ Easy to mix up — both are strings
function transferMoney(fromAccountId: string, toAccountId: string, amount: number) {}
transferMoney(toAccountId, fromAccountId, amount) // bug! args swapped, compiles fine
// ✅ Branded types catch this at compile time
type AccountId = string & { readonly __brand: "AccountId" }
function asAccountId(id: string): AccountId { return id as AccountId }
function transferMoney(from: AccountId, to: AccountId, amount: number) {}
// transferMoney(toId, fromId, amount) → TYPE ERROR if order matters semantically
Clean Architecture (Layered Design)
┌─────────────────────────────────────┐
│ Presentation (UI, Routes, Controllers) │ ← depends on ↓
├─────────────────────────────────────┤
│ Application (Use Cases, Services) │ ← depends on ↓
├─────────────────────────────────────┤
│ Domain (Entities, Business Rules) │ ← depends on nothing
├─────────────────────────────────────┤
│ Infrastructure (DB, External APIs) │ ← implements Domain interfaces
└─────────────────────────────────────┘
// domain/order.ts — pure business logic, no framework dependencies
export class Order {
constructor(
public readonly id: string,
public readonly items: OrderItem[],
private status: OrderStatus,
) {}
canBeCancelled(): boolean {
return this.status === "pending" || this.status === "processing"
}
cancel(): void {
if (!this.canBeCancelled()) {
throw new Error("Order cannot be cancelled in its current state")
}
this.status = "cancelled"
}
}
// application/cancel-order.usecase.ts — orchestrates domain + infra
export class CancelOrderUseCase {
constructor(private orderRepo: OrderRepository) {}
async execute(orderId: string): Promise<void> {
const order = await this.orderRepo.findById(orderId)
if (!order) throw new NotFoundError("Order not found")
order.cancel() // domain logic, throws if invalid
await this.orderRepo.save(order)
}
}
// infrastructure/postgres-order.repository.ts — implements the interface
export class PostgresOrderRepository implements OrderRepository {
async findById(id: string): Promise<Order | null> { /* SQL query */ }
async save(order: Order): Promise<void> { /* SQL update */ }
}
Horizontal Scaling Patterns
Stateless Services (Required for Horizontal Scaling)
// ❌ In-memory state — breaks with multiple instances
const sessions = new Map<string, Session>() // lost on restart, inconsistent across instances
// ✅ Externalize state to Redis/DB
async function getSession(id: string): Promise<Session | null> {
const data = await redis.get(`session:${id}`)
return data ? JSON.parse(data) : null
}
Load Balancing Considerations
[Load Balancer] → round-robin or least-connections
├── [Instance 1] — stateless, no local sessions/cache
├── [Instance 2] — stateless, no local sessions/cache
└── [Instance 3] — stateless, no local sessions/cache
│
├── [Shared Redis] — sessions, cache
└── [Shared PostgreSQL] — source of truth
Database Scaling Strategy
Step 1: Vertical scaling (bigger instance) — quick, limited ceiling
Step 2: Read replicas — scale reads, writes still single primary
Step 3: Connection pooling (pgBouncer) — handle more concurrent connections
Step 4: Caching layer (Redis) — reduce DB load for hot reads
Step 5: Sharding — only when truly necessary (high complexity cost)
// Read replica routing
const writeDb = drizzle(writePool) // INSERT/UPDATE/DELETE
const readDb = drizzle(readReplicaPool) // SELECT (eventually consistent)
async function getPost(id: string) {
return readDb.select().from(posts).where(eq(posts.id, id)) // use replica
}
async function createPost(data: NewPost) {
return writeDb.insert(posts).values(data).returning() // use primary
}
Caching Strategy
Request → [CDN/Edge Cache] → [App Cache (Redis)] → [Database]
(static assets) (computed results) (source of truth)
| Cache Layer |
TTL |
Invalidation |
| CDN (static assets) |
Days-weeks |
Content hash in filename |
| Edge (API responses) |
Seconds-minutes |
Tag-based revalidation |
| App cache (Redis) |
Minutes-hours |
Explicit invalidation on write |
| Query cache (DB) |
Seconds |
Automatic, DB-managed |
Code Review Checklist
Readability
Architecture
Scalability
Key Rules
- Rule of Three — don't abstract until you see the same pattern 3 times
- Guard clauses over nested ifs — flatten logic with early returns
- Domain logic has zero framework dependencies — pure business rules, testable in isolation
- Stateless services — externalize all state to Redis/DB for horizontal scaling
- Cache at the right layer — CDN for static, Redis for computed, never cache mutable user-specific data globally
- Paginate everything — no endpoint should return unbounded lists
- Index foreign keys and filter columns — the #1 cause of slow queries at scale
- Read replicas for read-heavy workloads — before reaching for sharding
- Branded types for domain primitives — prevents semantic mix-ups (IDs, money, etc.)
- SRP at the class/module level — one reason to change per unit