| name | scalability-clean-code |
| description | Guidelines for maintaining code readability (Clean Code, SOLID, DRY) and system scalability (Clean Architecture, loose coupling, horizontal scaling, caching). Use whenever the user is reviewing code quality, refactoring, designing system architecture for scale, or asking about best practices for maintainability. Trigger on mentions of clean code, SOLID principles, DRY, technical debt, code smells, refactoring, horizontal scaling, or system architecture review.
|
Scalability & Clean Code
Code readability principles and system-level scalability patterns.
SOLID Principles
Single Responsibility Principle
class UserService {
async createUser(data: UserInput) { }
}
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
function processPayment(method: string, amount: number) {
if (method === "stripe") { }
else if (method === "paypal") { }
}
interface PaymentProcessor {
process(amount: number): Promise<PaymentResult>
}
class StripeProcessor implements PaymentProcessor { }
class PayPalProcessor implements PaymentProcessor { }
class CryptoProcessor implements PaymentProcessor { }
class PaymentService {
constructor(private processor: PaymentProcessor) {}
process(amount: number) { return this.processor.process(amount) }
}
Dependency Inversion
class OrderService {
private db = new PostgresDatabase()
}
interface Database {
query<T>(sql: string, params: unknown[]): Promise<T[]>
}
class OrderService {
constructor(private db: Database) {}
}
const orderService = new OrderService(new PostgresDatabase())
const testService = new OrderService(new InMemoryDatabase())
DRY — Don't Repeat Yourself (But Don't Over-Abstract)
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")
}
const emailSchema = z.string().email()
function createUser(data: unknown) {
const validated = userCreateSchema.parse(data)
}
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
function processOrder(order: Order) {
if (order) {
if (order.items.length > 0) {
if (order.status === "pending") {
if (order.user.isVerified) {
}
}
}
}
}
function processOrder(order: Order) {
if (!order) return
if (order.items.length === 0) return
if (order.status !== "pending") return
if (!order.user.isVerified) return
}
Branded Types (Fix Primitive Obsession)
function transferMoney(fromAccountId: string, toAccountId: string, amount: number) {}
transferMoney(toAccountId, fromAccountId, amount)
type AccountId = string & { readonly __brand: "AccountId" }
function asAccountId(id: string): AccountId { return id as AccountId }
function transferMoney(from: AccountId, to: AccountId, amount: number) {}
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
└─────────────────────────────────────┘
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"
}
}
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()
await this.orderRepo.save(order)
}
}
export class PostgresOrderRepository implements OrderRepository {
async findById(id: string): Promise<Order | null> { }
async save(order: Order): Promise<void> { }
}
Horizontal Scaling Patterns
Stateless Services (Required for Horizontal Scaling)
const sessions = new Map<string, Session>()
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)
const writeDb = drizzle(writePool)
const readDb = drizzle(readReplicaPool)
async function getPost(id: string) {
return readDb.select().from(posts).where(eq(posts.id, id))
}
async function createPost(data: NewPost) {
return writeDb.insert(posts).values(data).returning()
}
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