Use when designing a new service from scratch, writing a tech spec or RFC, selecting a database or communication pattern, estimating capacity, or reviewing a design for scalability and reliability gaps.
Scanned 9/9/2026
Install to Claude Code
npx -y skills add kid-sid/codex-spellbook --skill system-design --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of System Design?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/kid-sid-system-design)More formats (shields.io, HTML) on the badges page.
---
name: system-design
description: Use when designing a new service from scratch, writing a tech spec or RFC, selecting a database or communication pattern, estimating capacity, or reviewing a design for scalability and reliability gaps.
---
# System Design
A structured end-to-end guide for designing systems from scratch, making architecture decisions, estimating capacity, and building for scalability and reliability.
## When to Activate
- Designing a new service, platform, or system from scratch
- Writing a design document, tech spec, or RFC
- Making a technology selection or architecture decision
- Writing an Architecture Decision Record (ADR)
- Reviewing a design for scalability, reliability, or security gaps
- Planning capacity for a feature expected to handle significant load
---
## Design Process
Follow these six steps in order. Each step informs the next — skipping steps leads to rework.
1. **Clarify requirements** — Gather functional requirements (what it does), non-functional requirements (scale, latency, availability, durability), and constraints (budget, existing stack, team size, timeline).
2. **Estimate scale** — QPS, storage, bandwidth. See [Capacity Estimation](#capacity-estimation) section.
3. **Define API contracts** — What APIs does the system expose? What are the request/response shapes, authentication mechanisms, and versioning strategy? Reference the `api-design` skill for REST/gRPC conventions.
4. **Design the data model** — Define entities, relationships, access patterns, and storage technology. Choose SQL vs NoSQL based on the [Technology Selection Matrix](#technology-selection-matrix).
5. **Design components** — Produce a High-Level Design (HLD) diagram, assign service responsibilities, and define communication patterns (sync vs async).
6. **Identify bottlenecks** — Find single points of failure, scaling limits, hot partitions, and cascading failure risks before the design is locked.
### Quick Decision Framework
| Non-functional requirement | Design implication |
|----------------------------|--------------------|
| High read throughput | Read replicas, caching layer |
| High write throughput | Sharding, async writes, CQRS |
| Low latency | CDN, in-process cache, co-location |
| High availability | Multi-AZ, load balancing, circuit breakers |
| Strong consistency | Single-leader DB, distributed transactions (use carefully) |
| Eventual consistency | Event-driven, CQRS + event sourcing acceptable |
---
## Capacity Estimation
Back-of-envelope estimation before any design work. Use these numbers to size components and catch obvious scaling problems early.
### Back-of-Envelope Template
```
# Traffic
Daily Active Users (DAU): X
Requests per user per day: Y
QPS (avg) = X * Y / 86400
QPS (peak) = avg * 3x
# Storage
Data per request: Z bytes
Daily new data = QPS_avg * 86400 * Z
1 year storage = daily * 365
With replication (3x): total * 3
# Bandwidth
Inbound = QPS * avg_request_size
Outbound = QPS * avg_response_size
```
### Example Estimation
```
# Twitter-scale write path example
DAU = 100M
Tweets per user per day = 0.5
QPS (avg) = 100M * 0.5 / 86400 ≈ 580 QPS
QPS (peak) = 580 * 3 ≈ 1,750 QPS
Tweet size = 300 bytes
Daily new data = 580 * 86400 * 300 ≈ 15 GB/day
1 year = 15 * 365 ≈ 5.5 TB
With 3x replication ≈ 16.5 TB/year
```
### Latency Reference Numbers
| Operation | Latency |
|-----------|---------|
| L1 cache | ~1 ns |
| L2 cache | ~10 ns |
| RAM read | ~100 ns |
| SSD read | ~100 µs |
| Network round trip (same DC) | ~500 µs |
| Network round trip (cross-region) | ~30–100 ms |
| HDD seek | ~10 ms |
Internalize these numbers. When someone says "just add a DB call", that is ~100 µs on SSD minimum — more if the query is complex or the DB is cross-region.
---
## High-Level Design
### Component Types
| Component | Responsibility | When to add |
|-----------|---------------|-------------|
| Load Balancer | Distribute traffic, health checks, SSL termination | Multiple app instances |
| API Gateway | Auth, rate limiting, routing, protocol translation | Public-facing APIs, microservices |
| Application Server | Business logic | Always |
| Cache (Redis/Memcached) | Reduce DB reads, session storage | Hot data, session state |
| Relational DB | ACID transactions, structured data | Most workloads |
| NoSQL DB | Flexible schema, high write throughput, time series | Specific access patterns |
| Message Queue | Async processing, decoupling, fan-out | Background jobs, event-driven flows |
| CDN | Static asset delivery, edge caching | Web apps, high-read global content |
| Object Storage (S3) | Files, images, backups | Binary data, large files |
| Search Engine (Elasticsearch) | Full-text search, complex queries | Search, log analytics |
### Diagram Conventions (text-based)
Use ASCII block diagrams when Mermaid is unavailable. Vertical lines show primary request paths; horizontal lines show async or secondary flows.
```
Client
│
▼
[CDN]──────────────────────────────────┐
│ │
▼ Static
[Load Balancer] Assets
│
├──► [App Server 1]
├──► [App Server 2] ──► [Cache (Redis)]
└──► [App Server N]
│
▼
[Primary DB] ──► [Read Replica 1]
──► [Read Replica 2]
```
### Adding a Message Queue
```
[App Server]
│
▼
[Message Queue (Kafka/SQS)]
│
├──► [Worker 1: Email notifications]
├──► [Worker 2: Analytics pipeline]
└──► [Worker 3: Search indexing]
```
Decouple producers and consumers. App servers enqueue and return immediately; workers process asynchronously without blocking the request path.
---
## Low-Level Design
### Sequence Diagrams (Mermaid)
Use Mermaid sequence diagrams to describe multi-service interactions. Always show the happy path first, then error cases separately.
**Example: Authentication flow**
```mermaid
sequenceDiagram
participant Client
participant API
participant AuthService
participant DB
Client->>API: POST /auth/login {email, password}
API->>AuthService: validate(email, password)
AuthService->>DB: SELECT user WHERE email=?
DB-->>AuthService: user record
AuthService->>AuthService: bcrypt.verify(password, hash)
AuthService-->>API: {userId, roles}
API-->>Client: {access_token, refresh_token}
```
**Example: Token refresh error path**
```mermaid
sequenceDiagram
participant Client
participant API
participant AuthService
Client->>API: POST /auth/refresh {refresh_token}
API->>AuthService: validate_refresh(token)
AuthService-->>API: TokenExpiredError
API-->>Client: 401 Unauthorized {error: "token_expired"}
```
### Class/Interface Design
Define interfaces at service boundaries, not at implementation boundaries. Depend on abstractions; never depend on concrete classes across module boundaries.
**UserRepository interface**
```python
from abc import ABC, abstractmethod
from typing import Optional
from uuid import UUID
class UserRepository(ABC):
@abstractmethod
def find_by_id(self, user_id: UUID) -> Optional["User"]:
...
@abstractmethod
def find_by_email(self, email: str) -> Optional["User"]:
...
@abstractmethod
def save(self, user: "User") -> "User":
...
@abstractmethod
def delete(self, user_id: UUID) -> None:
...
```
**PostgresUserRepository implementation**
```python
class PostgresUserRepository(UserRepository):
def __init__(self, db: Session):
self._db = db
def find_by_id(self, user_id: UUID) -> Optional[User]:
return self._db.query(UserModel).filter_by(id=user_id).first()
def find_by_email(self, email: str) -> Optional[User]:
return self._db.query(UserModel).filter_by(email=email).first()
def save(self, user: User) -> User:
self._db.merge(user)
self._db.commit()
return user
def delete(self, user_id: UUID) -> None:
self._db.query(UserModel).filter_by(id=user_id).delete()
self._db.commit()
```
The service layer only imports `UserRepository`. Swapping Postgres for DynamoDB requires only a new implementation class — no changes to the service.
### State Machines
Model entities as state machines when they have a well-defined lifecycle with discrete states and transitions. Common examples: orders, subscriptions, payments, onboarding flows.
**When to use a state machine:**
- The entity has more than two states
- Transitions have side effects (send email, charge card, create record)
- Invalid transitions must be rejected
**Order lifecycle example**
| Current State | Event | Next State | Action |
|---------------|-------|------------|--------|
| PENDING | payment_confirmed | PAID | Send order confirmation email |
| PAID | items_shipped | SHIPPED | Send shipping notification |
| SHIPPED | delivery_confirmed | DELIVERED | Release funds to merchant |
| PAID | cancellation_requested | CANCELLED | Issue refund |
| SHIPPED | cancellation_requested | REFUND_PENDING | Initiate return process |
| DELIVERED | refund_requested | REFUND_PENDING | Start refund review |
Store the current state in the database. Reject any event that does not have a valid transition from the current state. Log every transition with timestamp and actor.
---
## Architecture Decision Records (ADRs)
An ADR captures the context, decision, and consequences of a significant architecture choice. Write one whenever you make a decision that would be expensive or disruptive to reverse.
### Template
```markdown
# ADR-NNNN: [Short title]
## Status
[Proposed | Accepted | Deprecated | Superseded by ADR-XXXX]
## Context
[What is the problem? What forces are at play?]
## Decision
[What have we decided to do?]
## Consequences
### Positive
- ...
### Negative
- ...
## Alternatives Considered
| Option | Pros | Cons | Reason rejected |
|--------|------|------|-----------------|
| ... | ... | ... | ... |
```
### ADR Conventions
- **File naming:** `docs/adr/0001-use-postgres-for-primary-store.md`
- **Numbering:** Sequential, zero-padded to four digits. Never renumber existing ADRs.
- **Lifecycle:** `Proposed` → `Accepted` → (`Deprecated` | `Superseded`)
- **Superseded ADRs:** Keep the file. Add a note at the top: "Superseded by ADR-0012." Link forward, never delete.
- **Write an ADR when:** changing a primary database, switching communication patterns, adopting a new framework, changing authentication strategy, or adding a new external dependency that will be hard to remove.
- **Do not write an ADR for:** library version bumps, minor refactors, tooling preferences with no architectural impact.
### Example ADR
```markdown
# ADR-0003: Use PostgreSQL for primary data store
## Status
Accepted
## Context
We need a primary relational store. The team has strong SQL expertise.
Our access patterns are mostly relational with complex join queries.
We need ACID transactions for financial operations.
## Decision
Use PostgreSQL 15 as the primary relational database.
## Consequences
### Positive
- Full ACID compliance
- Rich query planner and index types (BRIN, GIN, partial)
- Strong community and tooling ecosystem
### Negative
- Vertical scaling only for writes (mitigated with read replicas)
- Schema migrations require care at scale
## Alternatives Considered
| Option | Pros | Cons | Reason rejected |
|-------------|-----------------------------|----------------------------------|-------------------------------|
| MySQL 8 | Widely supported | Weaker JSON support, less ANSI | Team unfamiliar, fewer features |
| MongoDB | Flexible schema | No multi-doc ACID, weak joins | Access patterns are relational |
| CockroachDB | Distributed SQL, geo-local | Operational complexity, cost | Premature for current scale |
```
---
## Technology Selection Matrix
### SQL vs NoSQL
| Criterion | SQL (PostgreSQL) | Document (MongoDB) | Key-Value (Redis) | Column (Cassandra) |
|-----------|------------------|--------------------|-------------------|--------------------|
| ACID transactions | Full | Limited | No | Lightweight |
| Query flexibility | High (joins, aggregates) | Medium | Low | Low |
| Schema | Strict | Flexible | None | Flexible |
| Scale-out | Vertical + read replicas | Horizontal | Horizontal | Horizontal |
| Best for | Most apps, financial data | Flexible documents | Cache, sessions | High-write, time series |
**Default:** Start with PostgreSQL. Move to NoSQL only when you have a concrete access pattern that PostgreSQL cannot handle at scale.
### Sync vs Async Communication
| Pattern | Latency | Coupling | Best for |
|---------|---------|----------|----------|
| REST/gRPC (sync) | Low | Tight | Request/response, queries |
| Message queue (async) | Higher | Loose | Background jobs, fan-out, retry |
| Event streaming (Kafka) | Medium | Very loose | Audit log, real-time analytics, event sourcing |
**Rule of thumb:** If the caller needs the result to continue, use sync. If the caller only needs to know the work was accepted, use async.
### Monolith vs Microservices
| Factor | Monolith | Microservices |
|--------|----------|---------------|
| Team size | < 10 engineers | > 10 engineers with clear domain ownership |
| Deploy complexity | Low | High (orchestration required) |
| Data isolation | Shared DB (simple) | DB per service (complex) |
| Scalability | Scale whole app | Scale individual services |
| Start with | Always | Only when monolith has real pain points |
**Default:** Start with a modular monolith. Extract services only when a specific bounded context has meaningfully different scaling or deployment needs.
---
## Scalability Patterns
### Horizontal Scaling
Make services stateless so any instance can handle any request.
- Store session state in Redis, not in-process memory
- Store uploaded files in object storage (S3), not on the local filesystem
- Store configuration in environment variables or a config service
- Never use sticky sessions in a load balancer unless absolutely required
```
# Stateless service checklist
- No in-process session state
- No local file system dependencies
- Idempotent request handling (safe to retry)
- Config from environment, not hardcoded
```
### Read Replicas
Route read-heavy queries to replicas to offload the primary.
```
[App Server]
│
├──[WRITE]──► [Primary DB]
│
└──[READ]───► [Read Replica 1]
[Read Replica 2]
```
- Accept replication lag: reads from replicas may be slightly stale
- Use the primary for reads immediately following a write (read-your-writes consistency)
- Monitor replication lag — alert if it exceeds your SLA tolerance
### Sharding
Partition data across multiple database nodes when a single node cannot handle write throughput or storage.
- **Hash sharding:** Apply a hash function to the shard key (e.g., `user_id % N`). Provides even distribution. Use consistent hashing to reduce rebalancing cost.
- **Range sharding:** Partition by ordered key (e.g., `created_at` by month). Efficient for time-series scans. Risk: hot partitions at the current time range.
- **Directory sharding:** A lookup table maps keys to shards. Flexible but lookup table becomes a bottleneck.
**Sharding problems to plan for:**
- Cross-shard queries require scatter-gather — expensive
- Rebalancing when adding shards is operationally complex
- Unique ID generation must be shard-aware (use UUIDs or Snowflake IDs)
### CQRS (Command Query Responsibility Segregation)
Separate the write model (commands) from the read model (queries).
```
┌────────────────────────┐
Write Path │ │ Read Path
│ │
[Command] ──► [Write Model] ──► [Event Bus] ──► [Projections] ──► [Read Model]
(normalized, (denormalized,
ACID DB) query-optimized)
```
- **Write model:** Normalized, strongly consistent, ACID transactions
- **Read model:** Denormalized projections tailored to specific query shapes
- **Event sourcing on the write side:** Store events (facts) rather than current state; derive state by replaying events
- **Use when:** Read and write access patterns are fundamentally different, or you need audit history
### Caching Layers
| Layer | Technology | Scope | Invalidation |
|-------|-----------|-------|--------------|
| L1 | In-process LRU (e.g., `functools.lru_cache`) | Single process | TTL or restart |
| L2 | Distributed cache (Redis, Memcached) | All app instances | TTL, event-driven, write-through |
| L3 | CDN (Cloudflare, CloudFront) | Public content, global edge | Cache-Control headers, purge API |
**Cache invalidation strategies:**
- **TTL (time-to-live):** Simple, tolerates stale data. Good for reference data.
- **Write-through:** Write to cache and DB simultaneously. Cache is never stale but adds write latency.
- **Event-driven invalidation:** On write, publish an event; consumers invalidate their cache entries. Complex but accurate.
- **Cache-aside (lazy loading):** Read from cache; on miss, read from DB and populate cache. Most common pattern.
---
## Reliability Patterns
### Circuit Breaker
Prevents cascading failures when a downstream dependency degrades or fails.
**States:**
- **Closed (normal):** Requests flow through. Failures are counted.
- **Open (failing):** Circuit trips after threshold failures. Requests are rejected immediately (fast fail) without calling the downstream.
- **Half-Open (probing):** After a cooldown period, a small number of requests are allowed through. If they succeed, the circuit closes. If they fail, it reopens.
```
[App] ──► [Circuit Breaker] ──► [Downstream Service]
│
└── If OPEN: return fallback immediately
```
**Libraries:**
- Python: `circuitbreaker`
- Node.js: `opossum`
- Go: `gobreaker`
- Java: Resilience4j
### Retry with Exponential Backoff
```
wait = base_delay * (2 ^ attempt) + jitter
max_attempts = 3–5
```
- **Jitter:** Add random noise (±50% of computed wait) to prevent the thundering herd problem — all clients retrying simultaneously
- **Only retry idempotent operations:** GET, PUT, DELETE are safe. POST may not be unless you add idempotency keys.
- **Set a budget:** Total retry time must be less than the upstream timeout.
```python
import random
import time
def retry_with_backoff(fn, max_attempts=4, base_delay=0.5):
for attempt in range(max_attempts):
try:
return fn()
except RetryableError as e:
if attempt == max_attempts - 1:
raise
wait = base_delay * (2 ** attempt) + random.uniform(0, base_delay)
time.sleep(wait)
```
### Bulkhead
Isolate resource pools so that saturation in one consumer does not exhaust shared resources.
```
[App Server]
│
├── Thread Pool A (50 threads) ──► [Payment Service]
├── Thread Pool B (20 threads) ──► [Inventory Service]
└── Thread Pool C (10 threads) ──► [Recommendation Service]
```
- If the Recommendation Service hangs, Thread Pool C exhausts, but Thread Pool A and B are unaffected
- Apply bulkheads for any downstream dependency that could be slow or unreliable
- Size each pool based on expected concurrency and dependency SLA
### Graceful Degradation
Return useful (possibly stale) responses when live data is unavailable.
| Scenario | Degraded response |
|----------|------------------|
| Inventory service down | Show product listings from cache, hide real-time stock count |
| Recommendation engine down | Show static "popular items" list |
| Search service down | Disable search box, show browse-by-category fallback |
| Payment service degraded | Queue payment, confirm async, inform user |
**Implementation pattern:** Wrap dependency calls in a try/except or circuit breaker. On failure, return the last cached value or a safe default.
### Timeout Hierarchy
Always set explicit timeouts at every layer. Never rely on defaults — most frameworks default to infinite or very large timeouts.
| Layer | Recommended timeout |
|-------|-------------------|
| User-facing API (client → server) | 500 ms – 2 s |
| Internal service-to-service | 100 – 500 ms |
| Database queries | 5 – 30 s (enforce with `statement_timeout`) |
| Async job processing | Set per-job based on SLA |
**Timeout budget rule:** The sum of downstream timeouts in a synchronous call chain must be less than the upstream timeout. If service A calls B calls C, then `timeout(C) + timeout(B overhead) < timeout(B)`, and so on up the chain.
---
## Red Flags
- **Starting with the data model instead of API contracts** — the schema should follow the access patterns, not the other way around; design API and user journeys first, then derive the schema
- **Microservices for a greenfield system** — premature decomposition introduces distributed-systems overhead before domain boundaries are understood; start as a modular monolith
- **Ignoring the CAP theorem tradeoff** — every distributed system is either CP or AP under partition; make the choice explicit, document it, and design client error handling accordingly
- **Sharding without modeling both write and read access patterns** — a shard key that distributes writes evenly can cause hot-spot reads; model all query patterns before committing to a key
- **Read replicas for all reads** — replication lag means replica reads return stale data; never read from a replica immediately after a write in the same request flow
- **No failure-mode analysis at design time** — designing for the happy path and deferring failure handling to implementation produces brittle systems; define timeout, retry, and circuit-breaker policies in the design
- **ADR skipped because the decision "feels obvious"** — obvious decisions are often the hardest to reverse; write the ADR before implementation starts, including rejected alternatives
## Checklist
- [ ] Requirements clarified: functional, non-functional, constraints
- [ ] Capacity estimated: QPS, storage, bandwidth for 1x and 10x load
- [ ] Single points of failure identified and mitigated
- [ ] Database choice justified with a decision table or ADR
- [ ] Caching strategy defined for read-heavy paths
- [ ] Async communication used for non-blocking operations
- [ ] Authentication and authorization designed (not an afterthought)
- [ ] ADR written for every significant technology or architecture decision
- [ ] Monitoring and alerting considered in the design
- [ ] Disaster recovery: RTO and RPO defined
- [ ] Timeout, retry, and circuit breaker policies defined for all external calls
- [ ] Design reviewed by at least one other engineer
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!