# Scalability Clean Code

> 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.

- Skill: `roedyrustam/scalability-clean-code-2` (Agent Skill)
- Install (CLI): `npx skillmds@latest add roedyrustam/scalability-clean-code-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/roedyrustam/scalability-clean-code-2/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: roedyrustam (https://skillmd.com/u/roedyrustam)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/roedyrustam/scalability-clean-code-2

---


# Scalability & Clean Code

Code readability principles and system-level scalability patterns.

---

## SOLID Principles

### Single Responsibility Principle
```typescript
// ❌ 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
```typescript
// ❌ 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
```typescript
// ❌ 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)

```typescript
// ❌ 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
```typescript
// ❌ 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)
```typescript
// ❌ 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
└─────────────────────────────────────┘
```

```typescript
// 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)
```typescript
// ❌ 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)
```

```typescript
// 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
- [ ] Function names describe what they do (no need to read the body to guess)
- [ ] No function longer than ~40 lines (extract if so)
- [ ] No more than 3 levels of nesting
- [ ] Comments explain *why*, not *what* (code should be self-explanatory for "what")

### Architecture
- [ ] Business logic separated from framework/infrastructure code
- [ ] Dependencies point toward abstractions, not concrete implementations
- [ ] No circular dependencies between modules
- [ ] Services are stateless (no in-memory state that breaks horizontal scaling)

### Scalability
- [ ] No N+1 queries (check with query logging in dev)
- [ ] Expensive computations cached appropriately
- [ ] Pagination on all list endpoints (never return unbounded result sets)
- [ ] Database indexes on all foreign keys and frequently filtered columns

---

## Key Rules

1. **Rule of Three** — don't abstract until you see the same pattern 3 times
2. **Guard clauses over nested ifs** — flatten logic with early returns
3. **Domain logic has zero framework dependencies** — pure business rules, testable in isolation
4. **Stateless services** — externalize all state to Redis/DB for horizontal scaling
5. **Cache at the right layer** — CDN for static, Redis for computed, never cache mutable user-specific data globally
6. **Paginate everything** — no endpoint should return unbounded lists
7. **Index foreign keys and filter columns** — the #1 cause of slow queries at scale
8. **Read replicas for read-heavy workloads** — before reaching for sharding
9. **Branded types for domain primitives** — prevents semantic mix-ups (IDs, money, etc.)
10. **SRP at the class/module level** — one reason to change per unit

