# Optimization Patterns
Proven performance optimization strategies across the stack.
## Caching Strategies
### Cache Selection Decision Tree
```
What are you caching?
│
├─ Computation result (same input → same output)
│ ├─ In-process only
│ │ └─ In-memory cache (LRU map, memoization)
│ │ Eviction: LRU, LFU, TTL
│ │ Tools: lru-cache (Node), functools.lru_cache (Python), sync.Map (Go)
│ └─ Shared across processes/servers
│ └─ Redis / Memcached
│ TTL-based, key-value, sub-millisecond latency
│
├─ Database query result
│ ├─ Rarely changes, expensive to compute
│ │ └─ Materialized view (database-level cache)
│ │ Refresh: on schedule, on trigger, on demand
│ ├─ Changes with writes
│ │ └─ Cache-aside pattern (read: cache → DB, write: DB → invalidate cache)
│ └─ Read-heavy, tolerate slight staleness
│ └─ Read replica + cache with TTL
│
├─ API response
│ ├─ Same for all users
│ │ └─ CDN cache (Cloudflare, CloudFront)
│ │ Headers: Cache-Control, ETag, Last-Modified
│ ├─ Varies by user but cacheable
│ │ └─ Vary header + CDN or reverse proxy cache
│ └─ Personalized but expensive
│ └─ Server-side cache (Redis) with user-specific keys
│
├─ Static assets (JS, CSS, images)
│ └─ CDN + long cache + content-hashed filenames
│ Cache-Control: public, max-age=31536000, immutable
│ Bust cache by changing filename (app.a1b2c3.js)
│
└─ HTML pages
├─ Static content
│ └─ Pre-render at build time (SSG)
│ Cache-Control: public, max-age=3600
├─ Mostly static, some dynamic
│ └─ Stale-while-revalidate
│ Cache-Control: public, max-age=60, stale-while-revalidate=3600
└─ Fully dynamic
└─ Short TTL or no-cache + ETag for conditional requests
```
### Cache Invalidation Patterns
```
Pattern: TTL (Time-To-Live)
├─ Simplest approach: cache expires after N seconds
├─ Pro: no coordination needed, self-healing
├─ Con: stale data for up to TTL duration
└─ Best for: session data, config, rate limits
Pattern: Cache-Aside (Lazy Loading)
├─ Read: check cache → miss → query DB → populate cache
├─ Write: update DB → delete cache key (not update)
├─ Pro: only caches what's actually requested
├─ Con: first request after invalidation is slow (cache miss)
└─ Best for: general purpose, most common pattern
Pattern: Write-Through
├─ Write: update cache AND DB in same operation
├─ Pro: cache always consistent with DB
├─ Con: write latency increases, caches unused data
└─ Best for: read-heavy data that must be fresh
Pattern: Write-Behind (Write-Back)
├─ Write: update cache, async flush to DB
├─ Pro: fast writes, batch DB operations
├─ Con: data loss risk if cache crashes before flush
└─ Best for: high-write-throughput, non-critical data (counters, analytics)
Pattern: Event-Driven Invalidation
├─ Publish event on data change, subscribers invalidate caches
├─ Pro: low latency invalidation, decoupled
├─ Con: eventual consistency, event delivery guarantees needed
└─ Best for: microservices, distributed systems
```
### Cache Anti-Patterns
| Anti-Pattern | Problem | Fix |
|-------------|---------|-----|
| Cache without eviction | Memory grows unbounded | Set max size + LRU/LFU eviction |
| Thundering herd | Cache expires, all requests hit DB simultaneously | Mutex/singleflight, stale-while-revalidate |
| Cache stampede | Hot key expires under high load | Background refresh before expiry |
| Inconsistent cache + DB | Update DB, crash before invalidating cache | Delete cache first (slightly stale reads), or use distributed transactions |
| Caching errors | Error response cached, served to all users | Only cache successful responses, or cache with very short TTL |
| Over-caching | Too many cache layers, hard to debug | Cache at one layer, usually closest to consumer |
## Database Optimization
### Indexing Strategy
```
Index selection decision tree:
│
├─ Query uses WHERE clause
│ ├─ Single column filter
│ │ └─ B-tree index on that column
│ ├─ Multiple column filter (AND)
│ │ └─ Composite index (most selective column first)
│ │ CREATE INDEX idx ON table(col_a, col_b, col_c)
│ │ Left-prefix rule: this index covers (a), (a,b), (a,b,c) queries
│ └─ Text search (LIKE '%term%')
│ └─ Full-text index (not B-tree, which only helps prefix LIKE 'term%')
│
├─ Query uses ORDER BY
│ └─ Index matching ORDER BY columns avoids filesort
│ Combine with WHERE columns: INDEX(where_col, order_col)
│
├─ Query uses JOIN
│ └─ Index on join columns of the inner table
│ ON a.id = b.a_id → index on b.a_id
│
├─ Query uses GROUP BY / DISTINCT
│ └─ Index matching GROUP BY columns
│
└─ High cardinality vs low cardinality
├─ High cardinality (many unique values): good index candidate
└─ Low cardinality (few unique values, e.g., boolean): partial index
CREATE INDEX idx ON orders(status) WHERE status = 'pending'
```
### Query Optimization Patterns
```sql
-- AVOID: SELECT * (fetches unnecessary columns)
SELECT * FROM users WHERE id = 1;
-- PREFER: select only needed columns
SELECT id, name, email FROM users WHERE id = 1;
-- AVOID: N+1 queries
-- Python/ORM: for user in users: user.orders (fires query per user)
-- PREFER: eager loading
-- SELECT * FROM users JOIN orders ON users.id = orders.user_id
-- AVOID: OFFSET for pagination on large tables
SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 10000;
-- PREFER: cursor-based pagination
SELECT * FROM posts WHERE created_at < '2025-01-01' ORDER BY created_at DESC LIMIT 20;
-- AVOID: functions on indexed columns in WHERE
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';
-- PREFER: expression index or store normalized
CREATE INDEX idx_email_lower ON users(LOWER(email));
-- AVOID: implicit type conversion
SELECT * FROM users WHERE id = '123'; -- string vs integer
-- PREFER: correct types
SELECT * FROM users WHERE id = 123;
-- AVOID: correlated subqueries
SELECT *, (SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id) FROM users;
-- PREFER: JOIN with GROUP BY
SELECT users.*, COUNT(orders.id) FROM users LEFT JOIN orders ON users.id = orders.user_id GROUP BY users.id;
```
### Connection Pooling
```
Connection pool sizing:
│
├─ Too small
│ └─ Requests queue waiting for connections
│ Symptom: latency spikes, "connection pool exhausted" errors
│
├─ Too large
│ └─ Database overwhelmed with connections
│ Symptom: high memory usage on DB, mutex contention, slower queries
│
└─ Right size
└─ Formula: connections = (core_count * 2) + effective_spindle_count
For SSD: connections ≈ core_count * 2-3
For cloud DB (e.g., RDS): check instance limits
Tools:
- PostgreSQL: PgBouncer (transaction pooling, session pooling)
- MySQL: ProxySQL
- Java: HikariCP (fastest JVM pool)
- Python: SQLAlchemy pool (pool_size, max_overflow, pool_timeout)
- Node.js: built-in pool in pg, mysql2, knex
- Go: database/sql built-in pool (SetMaxOpenConns, SetMaxIdleConns)
```
## Frontend Optimization
### Code Splitting
```
When to split:
│
├─ Route-based splitting (most impactful)
│ └─ Each page/route is a separate chunk
│ React: React.lazy(() => import('./Page'))
│ Next.js: automatic per-page
│ Vue: defineAsyncComponent(() => import('./Page.vue'))
│
├─ Component-based splitting
│ └─ Heavy components loaded on demand
│ Modal dialogs, rich text editors, charts, maps
│
```
### Critical Rendering Path
```
Optimization checklist:
│
├─ Critical CSS
│ ├─ Inline above-the-fold CSS in