Split View: 시스템 디자인 면접 완전 가이드 2025: 대규모 시스템 설계, 확장성, 가용성 패턴
시스템 디자인 면접 완전 가이드 2025: 대규모 시스템 설계, 확장성, 가용성 패턴
- 서론
- 1. 시스템 디자인 면접 4단계 프레임워크
- 2. 확장성 (Scalability)
- 3. 로드 밸런싱 (Load Balancing)
- 4. 캐싱 (Caching)
- 5. 데이터베이스 설계
- 6. CAP 정리와 PACELC
- 7. 메시지 큐 (Message Queue)
- 8. 레이트 리미팅 (Rate Limiting)
- 9. 실전 설계 예제
- 10. Back-of-Envelope 추정
- 11. 흔한 실수와 팁
- 12. 퀴즈
- 참고 자료
서론
시스템 디자인 면접은 시니어 엔지니어 채용에서 가장 중요한 평가 항목입니다. 알고리즘 문제가 코딩 능력을 측정한다면, 시스템 디자인은 대규모 시스템을 설계하고 트레이드오프를 이해하는 종합적인 엔지니어링 역량을 평가합니다.
Google, Meta, Amazon, Netflix 등 빅테크 기업들은 4560분 동안 수백만수억 명의 사용자를 처리할 수 있는 시스템을 설계하도록 요구합니다. 이 면접에서 성공하려면 단순히 기술을 아는 것이 아니라, 요구사항을 분석하고 적절한 기술을 선택하며 트레이드오프를 명확히 설명할 수 있어야 합니다.
이 가이드에서는 시스템 디자인 면접의 4단계 프레임워크부터 확장성, 캐싱, 데이터베이스, 메시지 큐, 실전 설계 예제까지 체계적으로 다룹니다.
1. 시스템 디자인 면접 4단계 프레임워크
1.1 1단계: 요구사항 명확화 (5분)
면접에서 가장 중요한 첫 단계입니다. 바로 설계에 들어가는 것은 가장 흔한 실수입니다.
기능적 요구사항 (Functional Requirements):
- 핵심 기능은 무엇인가?
- 사용자가 수행할 주요 작업은?
- 입력과 출력은 무엇인가?
비기능적 요구사항 (Non-Functional Requirements):
- 예상 사용자 수 / DAU
- 읽기 대 쓰기 비율
- 지연 시간 요구사항
- 가용성 요구 수준 (99.9%? 99.99%?)
- 데이터 일관성 요구 수준
면접관: "URL 단축 서비스를 설계해 주세요."
좋은 질문 예시:
Q: "일간 활성 사용자(DAU)는 어느 정도인가요?"
A: "1억 명입니다."
Q: "URL 단축과 리디렉션 중 어느 것이 더 중요한가요?"
A: "리디렉션이 더 중요합니다. 읽기가 훨씬 많습니다."
Q: "단축 URL의 길이에 제한이 있나요?"
A: "가능한 한 짧게요."
Q: "커스텀 URL을 지원해야 하나요?"
A: "네, 선택적으로 지원해 주세요."
1.2 2단계: 상위 수준 설계 (15~20분)
요구사항을 바탕으로 전체 시스템의 큰 그림을 그립니다.
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ Client │────▶│ Load Balancer│────▶│ Web Server │
└──────────┘ └──────────────┘ └──────┬───────┘
│
┌────────────────────────┤
▼ ▼
┌──────────┐ ┌──────────────┐
│ Cache │ │ Database │
└──────────┘ └──────────────┘
이 단계에서 다뤄야 할 것:
- API 설계 (RESTful 엔드포인트)
- 데이터 모델 설계
- 주요 컴포넌트와 상호 관계
- 데이터 흐름
1.3 3단계: 상세 설계 (15~20분)
면접관이 관심 있는 특정 컴포넌트를 깊이 파고듭니다.
- 데이터베이스 스키마와 인덱싱 전략
- 캐싱 전략과 무효화
- 확장성 병목 지점과 해결 방안
- 장애 처리와 복구 방안
1.4 4단계: 마무리 (5분)
- 설계의 병목 지점 식별
- 향후 확장 방향 논의
- 모니터링/알림 전략
- 추가 기능 제안
2. 확장성 (Scalability)
2.1 수직 확장 (Vertical Scaling)
기존 서버의 하드웨어 사양을 올리는 방식입니다.
수직 확장 전: 수직 확장 후:
┌─────────────┐ ┌─────────────┐
│ 4 CPU │ │ 32 CPU │
│ 16GB RAM │ ──▶ │ 256GB RAM │
│ 500GB SSD │ │ 4TB SSD │
└─────────────┘ └─────────────┘
장점: 구현이 간단, 데이터 일관성 유지 쉬움, 소프트웨어 변경 불필요
단점: 물리적 한계 존재 (단일 서버의 성능 상한), 단일 장애 지점(SPOF), 비용이 지수적으로 증가
2.2 수평 확장 (Horizontal Scaling)
서버 수를 늘려 부하를 분산하는 방식입니다.
수평 확장:
┌─────────────┐
┌────▶│ Server 1 │
│ └─────────────┘
┌──────────┐ │ ┌─────────────┐
│ Load │──┼────▶│ Server 2 │
│ Balancer │ │ └─────────────┘
└──────────┘ │ ┌─────────────┐
├────▶│ Server 3 │
│ └─────────────┘
│ ┌─────────────┐
└────▶│ Server N │
└─────────────┘
장점: 이론적으로 무제한 확장 가능, 장애 격리, 점진적 확장
단점: 데이터 일관성 관리 복잡, 세션 관리 필요, 운영 복잡도 증가
2.3 Stateless 서비스 설계
수평 확장의 핵심은 서버를 Stateless로 만드는 것입니다.
Bad (Stateful):
┌──────────┐ session ┌──────────┐
│ Client │──────────▶│ Server A │ (Server A가 죽으면 세션 유실)
└──────────┘ └──────────┘
Good (Stateless):
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Client │────▶│ Any │────▶│ Redis │
└──────────┘ │ Server │ │ (Session) │
└──────────┘ └──────────┘
세션, 캐시, 상태 정보를 외부 저장소(Redis, Memcached)에 보관하면 어떤 서버든 동일하게 요청을 처리할 수 있습니다.
2.4 Auto-Scaling 전략
# AWS Auto Scaling 설정 예시
AutoScalingGroup:
MinSize: 2
MaxSize: 20
DesiredCapacity: 4
TargetTrackingScaling:
TargetValue: 70.0 # CPU 70% 유지
PredefinedMetricType: ASGAverageCPUUtilization
# 스케줄 기반 스케일링 (예측 가능한 트래픽)
ScheduledActions:
- Schedule: "0 8 * * MON-FRI" # 평일 오전 8시
MinSize: 6
- Schedule: "0 22 * * *" # 매일 오후 10시
MinSize: 2
| 전략 | 기반 | 적합한 상황 |
|---|---|---|
| Target Tracking | 메트릭 목표값 | CPU, 요청 수 기반 |
| Step Scaling | 임계값 단계 | 급격한 트래픽 변화 |
| Scheduled | 시간 기반 | 예측 가능한 패턴 |
| Predictive | ML 기반 | 반복적 패턴 |
3. 로드 밸런싱 (Load Balancing)
3.1 로드 밸런싱 알고리즘
1. Round Robin (순차 배분)
Request 1 → Server A
Request 2 → Server B
Request 3 → Server C
Request 4 → Server A (다시 처음부터)
2. Weighted Round Robin (가중 순차)
Server A (weight=5): 5개 요청 처리
Server B (weight=3): 3개 요청 처리
Server C (weight=2): 2개 요청 처리
3. Least Connections (최소 연결)
Server A: 10 connections ← 새 요청은 여기로
Server B: 25 connections
Server C: 18 connections
4. IP Hash (IP 기반 분배)
hash(client_ip) % server_count = target_server
→ 같은 클라이언트는 항상 같은 서버로
3.2 L4 vs L7 로드 밸런서
L4 (전송 계층):
┌────────┐ TCP/UDP ┌──────────┐
│ Client │──────────────▶│ L4 LB │── IP:Port 기반 분배
└────────┘ └──────────┘
속도 빠름, 단순함, SSL 오프로드 불가
L7 (애플리케이션 계층):
┌────────┐ HTTP/HTTPS ┌──────────┐
│ Client │──────────────▶│ L7 LB │── URL, Header, Cookie 기반 분배
└────────┘ └──────────┘
콘텐츠 기반 라우팅, SSL 종료, 더 유연함
| 특성 | L4 | L7 |
|---|---|---|
| 계층 | TCP/UDP | HTTP/HTTPS |
| 성능 | 매우 빠름 | 상대적으로 느림 |
| 라우팅 | IP/Port | URL, Header, Cookie |
| SSL 종료 | 불가 | 가능 |
| 사용 예 | AWS NLB | AWS ALB, Nginx |
3.3 Consistent Hashing
일반적인 해싱은 서버가 추가/제거될 때 거의 모든 키가 재배치됩니다. Consistent Hashing은 이 문제를 해결합니다.
일반 해싱:
hash(key) % 3 = 서버 번호
→ 서버 추가시: hash(key) % 4 = 대부분 다른 서버로!
Consistent Hashing (해시 링):
Server A
/ \
/ 0 \
/ \
Server D Server B
\ /
\ key /
\ ↓ /
Server C
→ 서버 추가시: 인접한 키만 이동 (전체의 약 1/N만)
가상 노드(Virtual Nodes)로 균등 분배:
class ConsistentHash:
def __init__(self, nodes, virtual_nodes=150):
self.ring = SortedDict()
self.virtual_nodes = virtual_nodes
for node in nodes:
for i in range(virtual_nodes):
key = self._hash(f"node-vn-i")
self.ring[key] = node
def get_node(self, data_key):
if not self.ring:
return None
hash_val = self._hash(data_key)
# 해시 링에서 시계 방향으로 가장 가까운 노드
idx = self.ring.bisect_right(hash_val)
if idx == len(self.ring):
idx = 0
return self.ring.peekitem(idx)[1]
def _hash(self, key):
return int(hashlib.md5(key.encode()).hexdigest(), 16)
4. 캐싱 (Caching)
4.1 캐싱 전략
1. Cache-Aside (Lazy Loading):
┌────────┐ 1.읽기 ┌───────┐
│ App │────────▶│ Cache │
└────┬───┘ miss └───────┘
│ 2.DB 읽기 ▲
▼ 3.캐시 저장
┌────────┐
│ DB │
└────────┘
2. Write-Through:
┌────────┐ 1.쓰기 ┌───────┐ 2.동기 쓰기 ┌────────┐
│ App │────────▶│ Cache │─────────────▶│ DB │
└────────┘ └───────┘ └────────┘
(항상 일관성 보장, 쓰기 지연 증가)
3. Write-Back (Write-Behind):
┌────────┐ 1.쓰기 ┌───────┐ 2.비동기 배치 쓰기 ┌────────┐
│ App │────────▶│ Cache │ ═════════════════▶│ DB │
└────────┘ └───────┘ └────────┘
(쓰기 성능 우수, 데이터 유실 위험)
4. Write-Around:
┌────────┐ 1.쓰기 ┌────────┐
│ App │────────▶│ DB │
└────┬───┘ └────────┘
│ 2.읽기시 캐시 로드
▼
┌───────┐
│ Cache │
└───────┘
4.2 Redis vs Memcached
# Redis - 다양한 데이터 구조 지원
import redis
r = redis.Redis(host='localhost', port=6379)
# String
r.set('user:1001:name', 'Alice', ex=3600) # 1시간 TTL
# Hash
r.hset('user:1001', mapping={
'name': 'Alice',
'email': 'alice@example.com',
'login_count': 42
})
# Sorted Set (리더보드)
r.zadd('leaderboard', {'player_a': 1500, 'player_b': 1800})
top_10 = r.zrevrange('leaderboard', 0, 9, withscores=True)
# Pub/Sub
r.publish('notifications', 'New message!')
| 특성 | Redis | Memcached |
|---|---|---|
| 데이터 구조 | String, Hash, List, Set, Sorted Set 등 | Key-Value만 |
| 지속성 | RDB/AOF 지원 | 없음 |
| 복제 | Master-Replica | 없음 |
| 클러스터 | Redis Cluster | 클라이언트 측 샤딩 |
| 메모리 효율 | 상대적으로 낮음 | 높음 (단순 구조) |
| 사용 사례 | 세션, 리더보드, 큐, Pub/Sub | 단순 캐싱 |
4.3 CDN (Content Delivery Network)
CDN이 없는 경우:
사용자(서울) ──── 5000km ────▶ Origin Server(미국)
RTT: ~200ms
CDN이 있는 경우:
사용자(서울) ──── 50km ────▶ CDN Edge(서울) ── cache hit
RTT: ~5ms
Cache Miss시:
사용자(서울) → CDN Edge(서울) → Origin(미국)
↓ 캐시 저장
다음 요청부터 Edge에서 응답
CDN 캐시 무효화 전략:
1. TTL 기반: Cache-Control: max-age=86400
2. 버전 관리: /static/app.v2.3.1.js
3. 수동 무효화: CloudFront Invalidation
4. 태그 기반: Surrogate-Key 헤더로 선택적 무효화
4.4 캐시 문제와 해결
1. Cache Stampede (캐시 쓰나미):
TTL 만료 → 수천 요청이 동시에 DB로
해결: 뮤텍스 락 + 사전 갱신
2. Cache Penetration (캐시 관통):
존재하지 않는 키 반복 조회 → 매번 DB 조회
해결: Bloom Filter + 빈 결과도 캐싱
3. Hot Key Problem:
특정 키에 트래픽 집중 → 단일 캐시 노드 과부하
해결: 로컬 캐시 + 키 복제 (key_1, key_2, ...)
5. 데이터베이스 설계
5.1 SQL vs NoSQL
| 기준 | SQL (RDBMS) | NoSQL |
|---|---|---|
| 데이터 모델 | 정규화된 테이블 | Document, Key-Value, Column, Graph |
| 스키마 | 고정 스키마 | 유연한 스키마 |
| 확장 방식 | 수직 확장 위주 | 수평 확장 용이 |
| 트랜잭션 | ACID 완벽 지원 | 제한적 (일부 지원) |
| 조인 | 효율적 지원 | 제한적/불가 |
| 적합한 경우 | 관계 복잡, 트랜잭션 중요 | 대용량, 유연한 스키마, 고성능 |
| 대표 제품 | PostgreSQL, MySQL | MongoDB, Cassandra, DynamoDB |
SQL 선택 기준:
- 데이터 관계가 복잡하고 조인이 빈번
- ACID 트랜잭션이 필수 (금융, 결제)
- 데이터 무결성이 최우선
- 스키마가 안정적
NoSQL 선택 기준:
- 데이터 구조가 자주 변경
- 초대량 데이터 (수 TB 이상)
- 매우 낮은 지연 시간 필요
- 수평 확장이 필수
- 비정형 데이터 (로그, 소셜 미디어)
5.2 데이터베이스 복제 (Replication)
Master-Slave 복제:
┌──────────┐ 동기/비동기 복제 ┌──────────┐
│ Master │ ═══════════════════▶│ Slave 1 │ ← 읽기 전용
│ (Write) │ ═══════════════════▶│ Slave 2 │ ← 읽기 전용
└──────────┘ └──────────┘
Multi-Master 복제:
┌──────────┐ ◀═══════════════▶ ┌──────────┐
│ Master 1 │ 양방향 복제 │ Master 2 │
│ (R/W) │ │ (R/W) │
└──────────┘ └──────────┘
(충돌 해결 필요)
5.3 데이터베이스 샤딩 (Sharding)
샤딩 전:
┌─────────────────────┐
│ Single Database │
│ 10TB, 지연 증가 │
└─────────────────────┘
샤딩 후 (Hash 기반):
hash(user_id) % 4 = shard_number
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Shard 0 │ │ Shard 1 │ │ Shard 2 │ │ Shard 3 │
│ 2.5TB │ │ 2.5TB │ │ 2.5TB │ │ 2.5TB │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
샤딩 전략 비교:
| 전략 | 장점 | 단점 |
|---|---|---|
| Range-based | 범위 쿼리 효율적 | 핫스팟 발생 가능 |
| Hash-based | 균등 분배 | 범위 쿼리 비효율 |
| Directory-based | 유연한 매핑 | 디렉토리가 SPOF |
| Geographic | 지역성 활용 | 지역 간 쿼리 어려움 |
샤딩 시 고려사항:
1. 조인 쿼리: 샤드 간 조인 불가 → 비정규화 또는 애플리케이션 레벨 조인
2. 리밸런싱: 데이터 증가시 샤드 재분배 → Consistent Hashing 활용
3. 핫스팟: 특정 샤드에 트래픽 집중 → 샤드 키 재설계
4. 트랜잭션: 분산 트랜잭션 → Saga 패턴 또는 2PC
5. 고유 ID: 글로벌 유니크 ID 필요 → Snowflake ID, UUID
5.4 비정규화 (Denormalization)
-- 정규화된 설계 (조인 필요):
SELECT u.name, o.total, p.name AS product_name
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id;
-- 비정규화된 설계 (단일 테이블):
-- orders 테이블에 user_name, product_name 포함
SELECT user_name, total, product_name
FROM denormalized_orders
WHERE user_id = 1001;
비정규화는 읽기 성능을 향상시키지만, 데이터 중복과 쓰기 복잡성을 증가시킵니다. 읽기가 월등히 많은 경우에 적합합니다.
6. CAP 정리와 PACELC
6.1 CAP Theorem
Consistency (일관성)
/ \
/ \
/ CA \
/ (single DC) \
/________________\
| |
CP | 분산 시스템 | AP
| P는 피할 수 없음 |
|__________________|
Availability
(가용성)
네트워크 파티션 발생시:
CP 시스템 → 일관성 유지, 일부 요청 거부
AP 시스템 → 가용성 유지, 일시적 불일치 허용
실제 시스템의 CAP 선택:
| 시스템 | 선택 | 이유 |
|---|---|---|
| 은행 계좌 | CP | 잔액 불일치 불가 |
| DNS | AP | 약간 오래된 레코드 허용 |
| MongoDB | CP | 강한 일관성 기본 |
| Cassandra | AP | 결국 일관성 (Eventual) |
| ZooKeeper | CP | 분산 코디네이션 |
6.2 PACELC 이론
CAP을 확장한 PACELC은 파티션이 없을 때의 트레이드오프도 설명합니다.
if (Partition) then
Choose between Availability and Consistency
else
Choose between Latency and Consistency
예시:
- DynamoDB: PA/EL (가용성 + 낮은 지연)
- MongoDB: PC/EC (일관성 우선)
- Cassandra: PA/EL (가용성 + 낮은 지연)
- PostgreSQL(single): PC/EC (일관성 + 일관성)
7. 메시지 큐 (Message Queue)
7.1 비동기 처리의 필요성
동기 처리:
Client → API → 이메일 전송(3s) → 이미지 리사이즈(2s) → 로그 저장(1s) → Response
총 6초 대기
비동기 처리:
Client → API → Message Queue → Response (즉시)
↓
┌──────┴──────┐
▼ ▼
Email Worker Image Worker
(3s) (2s)
총 0.1초 대기
7.2 Kafka vs RabbitMQ vs SQS
Apache Kafka:
┌──────────┐ ┌─────────────────────────┐
│ Producer │────▶│ Topic: orders │
└──────────┘ │ ┌─────┬─────┬─────┐ │
│ │ P0 │ P1 │ P2 │ │
│ └──┬──┴──┬──┴──┬──┘ │
└─────┼─────┼─────┼──────┘
▼ ▼ ▼
┌──────────────────────┐
│ Consumer Group A │
│ (각 파티션 1 소비자) │
└──────────────────────┘
| 특성 | Kafka | RabbitMQ | SQS |
|---|---|---|---|
| 모델 | 로그 기반 스트리밍 | AMQP 메시지 브로커 | 관리형 큐 |
| 처리량 | 초당 수백만 메시지 | 초당 수만 메시지 | 자동 확장 |
| 메시지 보존 | 설정 기간 보존 | 소비 후 삭제 | 14일 |
| 순서 보장 | 파티션 내 보장 | 큐 단위 보장 | FIFO 큐 사용시 |
| 소비 모델 | Pull (Consumer Poll) | Push + Pull | Pull (Long Polling) |
| 사용 사례 | 이벤트 스트리밍, 로그 | 작업 큐, RPC | 서버리스, 디커플링 |
7.3 메시지 전달 보장
1. At-Most-Once (최대 1회):
Producer → Broker (ACK 안 기다림)
빠르지만 메시지 유실 가능
사용: 로그, 메트릭 (유실 허용)
2. At-Least-Once (최소 1회):
Producer → Broker → ACK
ACK 실패시 재전송 → 중복 가능
사용: 대부분의 시스템 (멱등 처리 필요)
3. Exactly-Once (정확히 1회):
트랜잭션 기반 또는 멱등 키 사용
성능 오버헤드 있음
사용: 금융, 결제 (정확성 필수)
8. 레이트 리미팅 (Rate Limiting)
8.1 Token Bucket 알고리즘
import time
import threading
class TokenBucket:
def __init__(self, capacity, refill_rate):
self.capacity = capacity # 버킷 최대 토큰 수
self.tokens = capacity # 현재 토큰 수
self.refill_rate = refill_rate # 초당 추가 토큰
self.last_refill = time.time()
self.lock = threading.Lock()
def allow_request(self):
with self.lock:
now = time.time()
# 토큰 보충
elapsed = now - self.last_refill
self.tokens = min(
self.capacity,
self.tokens + elapsed * self.refill_rate
)
self.last_refill = now
# 요청 허용 여부
if self.tokens >= 1:
self.tokens -= 1
return True
return False
# 초당 10개 요청, 버스트 최대 20개
limiter = TokenBucket(capacity=20, refill_rate=10)
8.2 Sliding Window Log 알고리즘
import time
from collections import deque
class SlidingWindowLog:
def __init__(self, window_size, max_requests):
self.window_size = window_size # 윈도우 크기 (초)
self.max_requests = max_requests
self.requests = deque()
def allow_request(self):
now = time.time()
# 윈도우 밖의 오래된 요청 제거
while self.requests and self.requests[0] < now - self.window_size:
self.requests.popleft()
if len(self.requests) < self.max_requests:
self.requests.append(now)
return True
return False
# 1분에 최대 100개 요청
limiter = SlidingWindowLog(window_size=60, max_requests=100)
8.3 Sliding Window Counter
Sliding Window Counter (메모리 효율적):
이전 윈도우 카운트: 84
현재 윈도우 카운트: 36
윈도우 크기: 60초
현재 위치: 윈도우의 25% 지점
가중 카운트 = 84 * (1 - 0.25) + 36 = 84 * 0.75 + 36 = 99
max_requests = 100 → 허용 (99 < 100)
| 알고리즘 | 메모리 | 정확도 | 구현 복잡도 |
|---|---|---|---|
| Token Bucket | O(1) | 높음 | 낮음 |
| Leaky Bucket | O(1) | 높음 | 낮음 |
| Sliding Window Log | O(N) | 매우 높음 | 중간 |
| Sliding Window Counter | O(1) | 근사치 | 중간 |
| Fixed Window | O(1) | 낮음 | 매우 낮음 |
9. 실전 설계 예제
9.1 URL Shortener 설계
요구사항:
- 1억 DAU, 읽기:쓰기 = 100:1
- 단축 URL은 가능한 짧게 (7자)
- 커스텀 URL 지원
- 리디렉션 지연 시간 100ms 이하
Back-of-Envelope 계산:
쓰기: 1억 DAU * 평균 0.1 URL/day = 1000만/day
QPS(쓰기) = 10,000,000 / 86,400 = ~116 QPS
QPS(읽기) = 116 * 100 = ~11,600 QPS
피크 QPS = 11,600 * 3 = ~35,000 QPS
저장 용량 (5년):
10,000,000 * 365 * 5 = 182.5억 레코드
각 레코드 ~500 bytes → 182.5억 * 500B = ~9.1TB
시스템 아키텍처:
┌────────┐ ┌──────┐ ┌─────────────┐
│ Client │────▶│ LB │────▶│ API Server │
└────────┘ └──────┘ └──────┬──────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Redis │ │ MySQL │ │ Analytics│
│ Cache │ │ (Sharded)│ │ (Kafka) │
└──────────┘ └──────────┘ └──────────┘
URL 인코딩 전략:
import hashlib
import base64
def generate_short_url(long_url, counter):
# 방법 1: Base62 인코딩 (auto-increment ID)
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
short = ""
n = counter
while n > 0:
short = chars[n % 62] + short
n //= 62
return short # 예: "dK4x9Z"
# 방법 2: MD5 해시 + Base62 (처음 7자)
hash_val = hashlib.md5(long_url.encode()).hexdigest()
num = int(hash_val[:12], 16)
short = ""
for _ in range(7):
short = chars[num % 62] + short
num //= 62
return short
# 62^7 = 약 3.5조 개의 고유 URL 가능
9.2 Twitter/X 피드 설계
핵심 문제: Fan-out
Fan-out on Write (Push 모델):
사용자가 트윗 작성시 모든 팔로워의 타임라인에 미리 저장
User A 트윗 → Fan-out Service
├─▶ Follower 1의 타임라인 캐시에 추가
├─▶ Follower 2의 타임라인 캐시에 추가
└─▶ Follower N의 타임라인 캐시에 추가
장점: 읽기가 매우 빠름 (미리 계산됨)
단점: 팔로워가 많으면 쓰기 부하 엄청남
Fan-out on Read (Pull 모델):
타임라인 요청시 팔로잉 중인 사용자들의 트윗을 실시간 조합
User B 타임라인 요청 → 팔로잉 사용자 조회
├─▶ User X의 최신 트윗
├─▶ User Y의 최신 트윗
└─▶ User Z의 최신 트윗
→ 병합 + 정렬 → 반환
장점: 쓰기가 단순
단점: 읽기 시 여러 DB 조회 필요
Celebrity Problem 해결 (하이브리드):
일반 사용자 (팔로워 < 10,000): Fan-out on Write
셀러브리티 (팔로워 > 10,000): Fan-out on Read
타임라인 조회:
1. 캐시에서 미리 팬아웃된 트윗 가져오기
2. 팔로잉 중인 셀러브리티의 최신 트윗 조회
3. 두 결과를 병합 + 시간순 정렬
9.3 채팅 시스템 설계
WebSocket 기반 실시간 통신:
┌────────┐ WebSocket ┌─────────────┐
│ User A │◀══════════▶│ Chat │
└────────┘ │ Server │
│ Cluster │
┌────────┐ WebSocket │ │
│ User B │◀══════════▶│ │
└────────┘ └─────┬──────┘
│
┌────────┼────────┐
▼ ▼ ▼
┌────────┐ ┌──────┐ ┌────────┐
│ Redis │ │Kafka │ │Message │
│Presence│ │ │ │ DB │
└────────┘ └──────┘ └────────┘
메시지 순서 보장:
방법 1: 서버 타임스탬프 (단순하지만 시계 동기화 문제)
방법 2: Snowflake ID (시간 + 서버 + 시퀀스)
Snowflake ID 구조 (64비트):
┌─────────────┬────────┬──────────┬──────────────┐
│ Timestamp │Datactr │ Machine │ Sequence │
│ 41 bits │ 5 bits │ 5 bits │ 12 bits │
└─────────────┴────────┴──────────┴──────────────┘
→ 시간순 정렬 가능 + 전역 고유
→ 초당 4096 * 1024 = ~400만 ID 생성 가능
온라인 상태 (Presence) 관리:
Heartbeat 방식:
- 클라이언트가 5초마다 heartbeat 전송
- 30초 동안 heartbeat 없으면 offline 처리
- Redis에 마지막 heartbeat 시간 저장
상태 변경 알림:
- 변경시 해당 사용자의 친구 목록에 알림
- 친구가 많으면 Fan-out 부하 → 접속 중인 친구에게만 알림
9.4 Netflix/비디오 스트리밍 설계
비디오 업로드 파이프라인:
┌──────────┐ ┌──────────┐ ┌─────────────────┐
│ Uploader │───▶│ Object │───▶│ Transcoding │
│ │ │ Store │ │ Pipeline │
└──────────┘ │ (S3) │ │ │
└──────────┘ │ ┌───────────┐ │
│ │ 240p │ │
│ │ 480p │ │
│ │ 720p │ │
│ │ 1080p │ │
│ │ 4K │ │
│ └───────────┘ │
└────────┬────────┘
▼
┌─────────────────┐
│ CDN에 배포 │
│ (전 세계 Edge) │
└─────────────────┘
Adaptive Bitrate Streaming (ABR):
사용자 네트워크 상태에 따라 화질 자동 조정:
대역폭 10Mbps → 1080p 세그먼트 요청
대역폭 3Mbps → 480p 세그먼트로 전환
대역폭 회복 → 점진적으로 화질 올림
HLS (HTTP Live Streaming) 방식:
1. 비디오를 2~10초 세그먼트로 분할
2. 각 세그먼트를 여러 화질로 인코딩
3. m3u8 매니페스트 파일로 세그먼트 목록 제공
4. 클라이언트가 대역폭에 맞는 세그먼트 선택
10. Back-of-Envelope 추정
10.1 자주 사용하는 숫자
지연 시간:
- L1 캐시 참조: ~1ns
- L2 캐시 참조: ~4ns
- 메인 메모리 참조: ~100ns
- SSD 랜덤 읽기: ~150us
- HDD Seek: ~10ms
- 동일 DC 왕복: ~0.5ms
- 미국 ↔ 유럽 왕복: ~75ms
용량:
- 1 char = 1 byte (ASCII) / 2~4 bytes (UTF-8)
- 1 UUID = 36 bytes (문자열) / 16 bytes (바이너리)
- 1 이미지 (중간) = ~200KB
- 1 비디오 (1분, 720p) = ~50MB
일간 환산:
- 1일 = 86,400초 = ~100,000초 (간단 계산)
- 1백만 요청/일 = ~12 QPS
- 1억 요청/일 = ~1,200 QPS
10.2 QPS 계산 예시
Twitter 규모 추정:
- DAU: 3억 명
- 평균 트윗 조회: 100회/일
- 읽기 QPS = 3억 * 100 / 86,400 = ~347,000 QPS
- 피크 QPS = 347,000 * 3 = ~1,000,000 QPS
- 평균 트윗 작성: 2회/일
- 쓰기 QPS = 3억 * 2 / 86,400 = ~6,944 QPS
저장소:
- 트윗 크기: ~300 bytes (텍스트 + 메타데이터)
- 하루: 6억 트윗 * 300B = ~180GB/일
- 5년: 180GB * 365 * 5 = ~328TB
- 미디어 포함: ~5배 = ~1.6PB
11. 흔한 실수와 팁
11.1 피해야 할 실수
1. 요구사항 확인 없이 바로 설계 시작
→ 반드시 5분은 질문에 투자
2. 하나의 컴포넌트에만 집중
→ 전체 그림을 먼저, 면접관이 요청한 부분을 깊이
3. 완벽한 설계를 추구
→ 트레이드오프를 설명하는 것이 더 중요
4. 숫자 없는 설계
→ QPS, 저장량, 대역폭을 반드시 계산
5. 일방적으로 말하기
→ 면접관과 대화하며 피드백 수용
6. 과도한 기술 도입
→ 문제에 맞는 적절한 기술 선택
11.2 면접 팁
1. 구조화된 접근:
- 4단계 프레임워크를 항상 따르기
- 각 단계에서 시간 관리
2. 트레이드오프 설명:
- "이 방법의 장점은 A이고, 단점은 B입니다."
- "대안으로 C가 있지만, 우리 상황에서는 D가 더 적합합니다."
3. 실제 사례 언급:
- "Netflix는 이 문제를 X 방식으로 해결했습니다."
- "이전 프로젝트에서 비슷한 문제를 경험했습니다."
4. 화이트보드 활용:
- 깔끔한 다이어그램
- 데이터 흐름 화살표
- 숫자/메트릭 표시
5. 면접관 힌트 수용:
- 면접관의 질문은 곧 힌트
- "좋은 지적입니다, 그 부분을 개선하면..."
12. 퀴즈
Q1. 확장성
수평 확장과 수직 확장의 주요 차이점 3가지를 설명하세요.
답변:
- 비용 구조: 수직 확장은 고사양 장비로 비용이 급증하지만, 수평 확장은 저사양 장비를 추가하므로 선형적 비용 증가
- 장애 격리: 수직 확장은 단일 장애 지점(SPOF)이지만, 수평 확장은 한 서버 장애가 전체에 영향 없음
- 확장 한계: 수직 확장은 물리적 한계가 있지만, 수평 확장은 이론적으로 무제한
수평 확장의 단점은 데이터 일관성 관리, 세션 관리, 운영 복잡도가 증가한다는 것입니다.
Q2. CAP 정리
은행 시스템은 왜 CP를 선택하나요? AP를 선택하면 어떤 문제가 발생하나요?
답변: 은행 시스템이 CP를 선택하는 이유는 잔액 일관성이 절대적으로 중요하기 때문입니다. AP를 선택하면 네트워크 파티션 발생 시 두 노드에서 동시에 출금이 가능해져, 실제 잔액보다 더 많은 금액이 출금되는 이중 지출(Double Spending) 문제가 발생할 수 있습니다. 이는 금융 손실로 직결되므로, 가용성을 일부 포기하더라도 일관성을 보장해야 합니다.
Q3. 캐싱
Cache Stampede(캐시 쓰나미)란 무엇이며, 어떻게 방지하나요?
답변: Cache Stampede는 인기 있는 캐시 항목의 TTL이 만료될 때, 수많은 요청이 동시에 DB에 쏟아지는 현상입니다.
방지 방법:
- 뮤텍스/분산 락: 첫 번째 요청만 DB에서 가져오고, 나머지는 대기
- 사전 갱신: TTL 만료 전에 백그라운드에서 갱신 (TTL의 80% 시점)
- TTL 랜덤화: 동일 시간에 만료되지 않도록 약간의 랜덤값 추가
- 외부 갱신: 전용 워커가 주기적으로 캐시 갱신
Q4. 메시지 큐
Kafka와 RabbitMQ를 각각 어떤 상황에서 선택해야 하나요?
답변: Kafka 선택:
- 이벤트 스트리밍 (로그 수집, 실시간 분석)
- 높은 처리량 필요 (초당 수백만 메시지)
- 메시지 재처리 필요 (소비 후에도 보존)
- 이벤트 소싱 패턴
RabbitMQ 선택:
- 전통적인 작업 큐 (이메일 전송, 이미지 처리)
- 복잡한 라우팅 필요 (Exchange, Routing Key)
- 메시지 우선순위 필요
- RPC 패턴
- 메시지 단위 ACK 필요
핵심 차이: Kafka는 "이벤트 로그"이고, RabbitMQ는 "메시지 브로커"입니다.
Q5. 실전 설계
URL Shortener에서 Base62 인코딩을 사용하는 이유와 해시 충돌을 어떻게 처리하나요?
답변: Base62 사용 이유:
- URL-safe 문자만 사용 (0-9, a-z, A-Z)
- 62^7 = 약 3.5조 개의 고유 URL 생성 가능
- 짧은 문자열로 큰 숫자 표현 가능
해시 충돌 처리:
- Auto-increment ID + Base62: 충돌 없음 (추천)
- MD5/SHA256 + 앞 7자: 충돌 시 뒤에 문자 추가
- 충돌 감지 + 재시도: DB에서 중복 검사 후 다른 해시 시도
- Bloom Filter: 존재 여부 빠르게 확인, 충돌 시 재생성
실무에서는 방법 1 (분산 환경에서 Snowflake ID 사용)이 가장 신뢰성 있습니다.
참고 자료
- Alex Xu, "System Design Interview - An Insider's Guide" (2020)
- Alex Xu, "System Design Interview Volume 2" (2022)
- Martin Kleppmann, "Designing Data-Intensive Applications" (2017)
- Google SRE Book - https://sre.google/sre-book/table-of-contents/
- AWS Well-Architected Framework - https://aws.amazon.com/architecture/well-architected/
- Meta Engineering Blog - https://engineering.fb.com/
- Netflix Tech Blog - https://netflixtechblog.com/
- Uber Engineering Blog - https://eng.uber.com/
- Cassandra vs MongoDB vs DynamoDB - https://db-engines.com/en/system/Amazon+DynamoDB%3BCassandra%3BMongoDB
- Kafka Documentation - https://kafka.apache.org/documentation/
- Redis Documentation - https://redis.io/documentation
- Consistent Hashing Paper - Karger et al. (1997)
- Dynamo Paper - Amazon (2007)
- MapReduce Paper - Google (2004)
System Design Interview Complete Guide 2025: Scalable Architecture, Availability, and Design Patterns
- Introduction
- 1. The 4-Step System Design Interview Framework
- 2. Scalability
- 3. Load Balancing
- 4. Caching
- 5. Database Design
- 6. CAP Theorem and PACELC
- 7. Message Queues
- 8. Rate Limiting
- 9. Real-World Design Examples
- 10. Back-of-Envelope Estimation
- 11. Common Mistakes and Tips
- 12. Quiz
- References
Introduction
System design interviews are the most critical evaluation component in senior engineering hiring. While algorithm questions measure coding ability, system design evaluates comprehensive engineering capability -- the ability to architect large-scale systems and understand tradeoffs.
Companies like Google, Meta, Amazon, and Netflix ask candidates to design systems handling millions to hundreds of millions of users in 45-60 minutes. To succeed, you need more than technical knowledge -- you must analyze requirements, choose appropriate technologies, and clearly explain tradeoffs.
This guide systematically covers the 4-step framework, scalability, caching, databases, message queues, and real-world design examples.
1. The 4-Step System Design Interview Framework
1.1 Step 1: Clarify Requirements (5 minutes)
The most important first step. Jumping straight into design is the most common mistake.
Functional Requirements:
- What are the core features?
- What main actions will users perform?
- What are the inputs and outputs?
Non-Functional Requirements:
- Expected number of users / DAU
- Read-to-write ratio
- Latency requirements
- Availability level (99.9%? 99.99%?)
- Data consistency requirements
Interviewer: "Design a URL shortening service."
Good questions:
Q: "What's the expected DAU?"
A: "100 million."
Q: "Which is more important, URL shortening or redirection?"
A: "Redirection is more important. Reads are much more frequent."
Q: "Is there a length limit for shortened URLs?"
A: "As short as possible."
Q: "Should we support custom URLs?"
A: "Yes, optionally."
1.2 Step 2: High-Level Design (15-20 minutes)
Draw the big picture based on requirements.
+-----------+ +---------------+ +---------------+
| Client |---->| Load Balancer |---->| Web Server |
+-----------+ +---------------+ +------+--------+
|
+--------------------------+
v v
+-----------+ +---------------+
| Cache | | Database |
+-----------+ +---------------+
What to cover at this stage:
- API design (RESTful endpoints)
- Data model design
- Key components and their relationships
- Data flow
1.3 Step 3: Deep Dive (15-20 minutes)
Dig deep into specific components the interviewer is interested in.
- Database schema and indexing strategies
- Caching strategies and invalidation
- Scalability bottlenecks and solutions
- Failure handling and recovery
1.4 Step 4: Wrap Up (5 minutes)
- Identify design bottlenecks
- Discuss future scaling directions
- Monitoring/alerting strategy
- Suggest additional features
2. Scalability
2.1 Vertical Scaling
Upgrading hardware specifications of existing servers.
Before: After:
+---------------+ +---------------+
| 4 CPU | | 32 CPU |
| 16GB RAM | ---> | 256GB RAM |
| 500GB SSD | | 4TB SSD |
+---------------+ +---------------+
Pros: Simple implementation, easy data consistency, no software changes needed
Cons: Physical limits exist (single server ceiling), single point of failure (SPOF), costs increase exponentially
2.2 Horizontal Scaling
Adding more servers to distribute load.
+---------------+
+---->| Server 1 |
| +---------------+
+-----------+ | +---------------+
| Load |-+---->| Server 2 |
| Balancer | | +---------------+
+-----------+ | +---------------+
+---->| Server 3 |
| +---------------+
| +---------------+
+---->| Server N |
+---------------+
Pros: Theoretically unlimited scaling, fault isolation, incremental expansion
Cons: Data consistency management complexity, session management needed, operational complexity increase
2.3 Stateless Service Design
The key to horizontal scaling is making servers stateless.
Bad (Stateful):
+-----------+ session +-----------+
| Client |---------->| Server A | (Session lost if Server A dies)
+-----------+ +-----------+
Good (Stateless):
+-----------+ +-----------+ +-----------+
| Client |---->| Any |---->| Redis |
+-----------+ | Server | | (Session) |
+-----------+ +-----------+
By storing sessions, cache, and state in external stores (Redis, Memcached), any server can handle any request identically.
2.4 Auto-Scaling Strategies
# AWS Auto Scaling configuration example
AutoScalingGroup:
MinSize: 2
MaxSize: 20
DesiredCapacity: 4
TargetTrackingScaling:
TargetValue: 70.0 # Maintain 70% CPU
PredefinedMetricType: ASGAverageCPUUtilization
# Schedule-based scaling (predictable traffic)
ScheduledActions:
- Schedule: "0 8 * * MON-FRI" # Weekdays 8 AM
MinSize: 6
- Schedule: "0 22 * * *" # Daily 10 PM
MinSize: 2
| Strategy | Based On | Best For |
|---|---|---|
| Target Tracking | Metric target value | CPU, request count |
| Step Scaling | Threshold steps | Sudden traffic spikes |
| Scheduled | Time-based | Predictable patterns |
| Predictive | ML-based | Recurring patterns |
3. Load Balancing
3.1 Load Balancing Algorithms
1. Round Robin (Sequential Distribution)
Request 1 -> Server A
Request 2 -> Server B
Request 3 -> Server C
Request 4 -> Server A (back to start)
2. Weighted Round Robin
Server A (weight=5): handles 5 requests
Server B (weight=3): handles 3 requests
Server C (weight=2): handles 2 requests
3. Least Connections
Server A: 10 connections <-- new request goes here
Server B: 25 connections
Server C: 18 connections
4. IP Hash
hash(client_ip) % server_count = target_server
-> Same client always goes to same server
3.2 L4 vs L7 Load Balancers
L4 (Transport Layer):
+----------+ TCP/UDP +-----------+
| Client |--------------->| L4 LB |-- routes by IP:Port
+----------+ +-----------+
Fast, simple, no SSL offload
L7 (Application Layer):
+----------+ HTTP/HTTPS +-----------+
| Client |--------------->| L7 LB |-- routes by URL, Header, Cookie
+----------+ +-----------+
Content-based routing, SSL termination, more flexible
| Feature | L4 | L7 |
|---|---|---|
| Layer | TCP/UDP | HTTP/HTTPS |
| Performance | Very fast | Relatively slower |
| Routing | IP/Port | URL, Header, Cookie |
| SSL Termination | No | Yes |
| Examples | AWS NLB | AWS ALB, Nginx |
3.3 Consistent Hashing
Regular hashing redistributes almost all keys when servers are added or removed. Consistent hashing solves this problem.
Regular Hashing:
hash(key) % 3 = server number
-> Adding a server: hash(key) % 4 = most keys move!
Consistent Hashing (Hash Ring):
Server A
/ \
/ 0 \
/ \
Server D Server B
\ /
\ key /
\ | /
Server C
-> Adding a server: only adjacent keys move (~1/N of total)
Virtual Nodes for even distribution:
class ConsistentHash:
def __init__(self, nodes, virtual_nodes=150):
self.ring = SortedDict()
self.virtual_nodes = virtual_nodes
for node in nodes:
for i in range(virtual_nodes):
key = self._hash(f"node-vn-i")
self.ring[key] = node
def get_node(self, data_key):
if not self.ring:
return None
hash_val = self._hash(data_key)
# Find nearest node clockwise on the hash ring
idx = self.ring.bisect_right(hash_val)
if idx == len(self.ring):
idx = 0
return self.ring.peekitem(idx)[1]
def _hash(self, key):
return int(hashlib.md5(key.encode()).hexdigest(), 16)
4. Caching
4.1 Caching Strategies
1. Cache-Aside (Lazy Loading):
+--------+ 1.read +---------+
| App |---------->| Cache |
+---+----+ miss +---------+
| 2.read DB ^
v 3.store in cache
+--------+
| DB |
+--------+
2. Write-Through:
+--------+ 1.write +---------+ 2.sync write +--------+
| App |---------->| Cache |--------------->| DB |
+--------+ +---------+ +--------+
(Always consistent, higher write latency)
3. Write-Back (Write-Behind):
+--------+ 1.write +---------+ 2.async batch write +--------+
| App |---------->| Cache |======================>| DB |
+--------+ +---------+ +--------+
(Great write performance, data loss risk)
4. Write-Around:
+--------+ 1.write +--------+
| App |---------->| DB |
+---+----+ +--------+
| 2.load to cache on read
v
+---------+
| Cache |
+---------+
4.2 Redis vs Memcached
# Redis - supports various data structures
import redis
r = redis.Redis(host='localhost', port=6379)
# String
r.set('user:1001:name', 'Alice', ex=3600) # 1-hour TTL
# Hash
r.hset('user:1001', mapping={
'name': 'Alice',
'email': 'alice@example.com',
'login_count': 42
})
# Sorted Set (leaderboard)
r.zadd('leaderboard', {'player_a': 1500, 'player_b': 1800})
top_10 = r.zrevrange('leaderboard', 0, 9, withscores=True)
# Pub/Sub
r.publish('notifications', 'New message!')
| Feature | Redis | Memcached |
|---|---|---|
| Data Structures | String, Hash, List, Set, Sorted Set, etc. | Key-Value only |
| Persistence | RDB/AOF supported | None |
| Replication | Master-Replica | None |
| Clustering | Redis Cluster | Client-side sharding |
| Memory Efficiency | Relatively lower | Higher (simple structure) |
| Use Cases | Sessions, leaderboards, queues, Pub/Sub | Simple caching |
4.3 CDN (Content Delivery Network)
Without CDN:
User(Seoul) ---- 5000km ----> Origin Server(US)
RTT: ~200ms
With CDN:
User(Seoul) ---- 50km ----> CDN Edge(Seoul) -- cache hit
RTT: ~5ms
On Cache Miss:
User(Seoul) -> CDN Edge(Seoul) -> Origin(US)
| store in cache
Next request served from Edge
CDN Cache Invalidation Strategies:
1. TTL-based: Cache-Control: max-age=86400
2. Versioning: /static/app.v2.3.1.js
3. Manual invalidation: CloudFront Invalidation
4. Tag-based: Surrogate-Key header for selective purging
4.4 Cache Problems and Solutions
1. Cache Stampede (Thundering Herd):
TTL expires -> thousands of requests hit DB simultaneously
Solution: Mutex lock + pre-refresh
2. Cache Penetration:
Repeated queries for non-existent keys -> every request hits DB
Solution: Bloom Filter + cache empty results
3. Hot Key Problem:
Traffic concentrated on specific key -> single cache node overloaded
Solution: Local cache + key replication (key_1, key_2, ...)
5. Database Design
5.1 SQL vs NoSQL
| Criteria | SQL (RDBMS) | NoSQL |
|---|---|---|
| Data Model | Normalized tables | Document, Key-Value, Column, Graph |
| Schema | Fixed schema | Flexible schema |
| Scaling | Primarily vertical | Easy horizontal scaling |
| Transactions | Full ACID support | Limited (some support) |
| Joins | Efficient support | Limited/impossible |
| Best For | Complex relationships, transactions critical | Large volume, flexible schema, high performance |
| Products | PostgreSQL, MySQL | MongoDB, Cassandra, DynamoDB |
SQL Selection Criteria:
- Complex data relationships with frequent joins
- ACID transactions essential (finance, payments)
- Data integrity is top priority
- Stable schema
NoSQL Selection Criteria:
- Data structure changes frequently
- Massive data volume (multiple TB+)
- Very low latency required
- Horizontal scaling essential
- Unstructured data (logs, social media)
5.2 Database Replication
Master-Slave Replication:
+-----------+ sync/async replication +-----------+
| Master | =============================> | Slave 1 | <- Read-only
| (Write) | =============================> | Slave 2 | <- Read-only
+-----------+ +-----------+
Multi-Master Replication:
+-----------+ <=============================> +-----------+
| Master 1 | bidirectional replication | Master 2 |
| (R/W) | | (R/W) |
+-----------+ +-----------+
(Conflict resolution required)
5.3 Database Sharding
Before Sharding:
+-----------------------+
| Single Database |
| 10TB, latency grows |
+-----------------------+
After Sharding (Hash-based):
hash(user_id) % 4 = shard_number
+-----------+ +-----------+ +-----------+ +-----------+
| Shard 0 | | Shard 1 | | Shard 2 | | Shard 3 |
| 2.5TB | | 2.5TB | | 2.5TB | | 2.5TB |
+-----------+ +-----------+ +-----------+ +-----------+
Sharding Strategy Comparison:
| Strategy | Pros | Cons |
|---|---|---|
| Range-based | Efficient range queries | Hotspot potential |
| Hash-based | Even distribution | Inefficient range queries |
| Directory-based | Flexible mapping | Directory is SPOF |
| Geographic | Locality advantage | Cross-region queries difficult |
Sharding Considerations:
1. Join queries: Cross-shard joins impossible -> denormalization or application-level joins
2. Rebalancing: Shard redistribution on growth -> use Consistent Hashing
3. Hotspots: Traffic concentrated on specific shard -> redesign shard key
4. Transactions: Distributed transactions -> Saga pattern or 2PC
5. Unique IDs: Need globally unique IDs -> Snowflake ID, UUID
5.4 Denormalization
-- Normalized design (joins needed):
SELECT u.name, o.total, p.name AS product_name
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id;
-- Denormalized design (single table):
-- orders table includes user_name, product_name
SELECT user_name, total, product_name
FROM denormalized_orders
WHERE user_id = 1001;
Denormalization improves read performance but increases data duplication and write complexity. Best suited when reads significantly outnumber writes.
6. CAP Theorem and PACELC
6.1 CAP Theorem
Consistency
/ \
/ \
/ CA \
/ (single DC) \
/------------------\
| |
CP | Distributed Sys | AP
| P is unavoidable |
|--------------------|
Availability
During network partition:
CP system -> maintains consistency, rejects some requests
AP system -> maintains availability, allows temporary inconsistency
Real-World CAP Choices:
| System | Choice | Reason |
|---|---|---|
| Bank Accounts | CP | Balance inconsistency unacceptable |
| DNS | AP | Slightly stale records acceptable |
| MongoDB | CP | Strong consistency by default |
| Cassandra | AP | Eventual consistency |
| ZooKeeper | CP | Distributed coordination |
6.2 PACELC Theory
PACELC extends CAP by describing tradeoffs when there is no partition.
if (Partition) then
Choose between Availability and Consistency
else
Choose between Latency and Consistency
Examples:
- DynamoDB: PA/EL (availability + low latency)
- MongoDB: PC/EC (consistency-first)
- Cassandra: PA/EL (availability + low latency)
- PostgreSQL(single): PC/EC (consistency + consistency)
7. Message Queues
7.1 The Need for Asynchronous Processing
Synchronous Processing:
Client -> API -> Send Email(3s) -> Resize Image(2s) -> Log(1s) -> Response
Total: 6 seconds waiting
Asynchronous Processing:
Client -> API -> Message Queue -> Response (immediately)
|
+------+------+
v v
Email Worker Image Worker
(3s) (2s)
Total: 0.1 seconds waiting
7.2 Kafka vs RabbitMQ vs SQS
Apache Kafka:
+-----------+ +---------------------------+
| Producer |---->| Topic: orders |
+-----------+ | +-------+-------+------+ |
| | P0 | P1 | P2 | |
| +---+---+---+---+--+---+ |
+------+-------+------+-----+
v v v
+------------------------+
| Consumer Group A |
| (one consumer per |
| partition) |
+------------------------+
| Feature | Kafka | RabbitMQ | SQS |
|---|---|---|---|
| Model | Log-based streaming | AMQP message broker | Managed queue |
| Throughput | Millions/sec | Tens of thousands/sec | Auto-scaling |
| Message Retention | Configurable retention | Deleted after consumption | 14 days |
| Ordering | Guaranteed within partition | Per-queue guarantee | With FIFO queue |
| Consumption Model | Pull (Consumer Poll) | Push + Pull | Pull (Long Polling) |
| Use Cases | Event streaming, logs | Task queues, RPC | Serverless, decoupling |
7.3 Message Delivery Guarantees
1. At-Most-Once:
Producer -> Broker (no ACK wait)
Fast but messages can be lost
Use: Logs, metrics (loss acceptable)
2. At-Least-Once:
Producer -> Broker -> ACK
Retry on ACK failure -> duplicates possible
Use: Most systems (idempotent processing needed)
3. Exactly-Once:
Transaction-based or idempotency keys
Performance overhead
Use: Finance, payments (accuracy critical)
8. Rate Limiting
8.1 Token Bucket Algorithm
import time
import threading
class TokenBucket:
def __init__(self, capacity, refill_rate):
self.capacity = capacity # Max tokens in bucket
self.tokens = capacity # Current tokens
self.refill_rate = refill_rate # Tokens added per second
self.last_refill = time.time()
self.lock = threading.Lock()
def allow_request(self):
with self.lock:
now = time.time()
# Refill tokens
elapsed = now - self.last_refill
self.tokens = min(
self.capacity,
self.tokens + elapsed * self.refill_rate
)
self.last_refill = now
# Check if request allowed
if self.tokens >= 1:
self.tokens -= 1
return True
return False
# 10 requests/sec, burst up to 20
limiter = TokenBucket(capacity=20, refill_rate=10)
8.2 Sliding Window Log Algorithm
import time
from collections import deque
class SlidingWindowLog:
def __init__(self, window_size, max_requests):
self.window_size = window_size # Window size (seconds)
self.max_requests = max_requests
self.requests = deque()
def allow_request(self):
now = time.time()
# Remove old requests outside window
while self.requests and self.requests[0] < now - self.window_size:
self.requests.popleft()
if len(self.requests) < self.max_requests:
self.requests.append(now)
return True
return False
# Max 100 requests per minute
limiter = SlidingWindowLog(window_size=60, max_requests=100)
8.3 Sliding Window Counter
Sliding Window Counter (memory efficient):
Previous window count: 84
Current window count: 36
Window size: 60 seconds
Current position: 25% into window
Weighted count = 84 * (1 - 0.25) + 36 = 84 * 0.75 + 36 = 99
max_requests = 100 -> allowed (99 < 100)
| Algorithm | Memory | Accuracy | Implementation |
|---|---|---|---|
| Token Bucket | O(1) | High | Low |
| Leaky Bucket | O(1) | High | Low |
| Sliding Window Log | O(N) | Very High | Medium |
| Sliding Window Counter | O(1) | Approximate | Medium |
| Fixed Window | O(1) | Low | Very Low |
9. Real-World Design Examples
9.1 URL Shortener Design
Requirements:
- 100M DAU, read:write = 100:1
- Shortened URLs as short as possible (7 chars)
- Custom URL support
- Redirection latency under 100ms
Back-of-Envelope Estimation:
Writes: 100M DAU * 0.1 URLs/day = 10M/day
Write QPS = 10,000,000 / 86,400 = ~116 QPS
Read QPS = 116 * 100 = ~11,600 QPS
Peak QPS = 11,600 * 3 = ~35,000 QPS
Storage (5 years):
10,000,000 * 365 * 5 = 18.25 billion records
Each record ~500 bytes -> 18.25B * 500B = ~9.1TB
System Architecture:
+----------+ +------+ +---------------+
| Client |---->| LB |---->| API Server |
+----------+ +------+ +------+--------+
|
+----------------+----------------+
v v v
+-----------+ +-----------+ +-----------+
| Redis | | MySQL | | Analytics |
| Cache | | (Sharded) | | (Kafka) |
+-----------+ +-----------+ +-----------+
URL Encoding Strategy:
import hashlib
def generate_short_url(long_url, counter):
# Method 1: Base62 encoding (auto-increment ID)
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
short = ""
n = counter
while n > 0:
short = chars[n % 62] + short
n //= 62
return short # e.g., "dK4x9Z"
# Method 2: MD5 hash + Base62 (first 7 chars)
hash_val = hashlib.md5(long_url.encode()).hexdigest()
num = int(hash_val[:12], 16)
short = ""
for _ in range(7):
short = chars[num % 62] + short
num //= 62
return short
# 62^7 = ~3.5 trillion unique URLs possible
9.2 Twitter/X Feed Design
Core Problem: Fan-out
Fan-out on Write (Push Model):
When user tweets -> pre-write to all followers' timelines
User A tweets -> Fan-out Service
+--> Follower 1's timeline cache
+--> Follower 2's timeline cache
+--> Follower N's timeline cache
Pros: Reads are very fast (pre-computed)
Cons: Enormous write load for users with many followers
Fan-out on Read (Pull Model):
When timeline requested -> assemble tweets in real-time
User B requests timeline -> Query followed users
+--> User X's latest tweets
+--> User Y's latest tweets
+--> User Z's latest tweets
-> merge + sort -> return
Pros: Simple writes
Cons: Multiple DB queries on every read
Celebrity Problem Solution (Hybrid):
Regular users (followers < 10,000): Fan-out on Write
Celebrities (followers > 10,000): Fan-out on Read
Timeline fetch:
1. Get pre-fanned-out tweets from cache
2. Query latest tweets from followed celebrities
3. Merge both results + sort by time
9.3 Chat System Design
WebSocket-based Real-time Communication:
+----------+ WebSocket +---------------+
| User A |<==========>| Chat |
+----------+ | Server |
| Cluster |
+----------+ WebSocket | |
| User B |<==========>| |
+----------+ +------+-------+
|
+-----------+-----------+
v v v
+----------+ +--------+ +----------+
| Redis | | Kafka | | Message |
| Presence | | | | DB |
+----------+ +--------+ +----------+
Message Ordering:
Method 1: Server timestamps (simple but clock sync issues)
Method 2: Snowflake ID (time + server + sequence)
Snowflake ID Structure (64-bit):
+---------------+----------+----------+--------------+
| Timestamp | Datactr | Machine | Sequence |
| 41 bits | 5 bits | 5 bits | 12 bits |
+---------------+----------+----------+--------------+
-> Time-sortable + globally unique
-> Can generate ~4 million IDs/sec
Presence Management:
Heartbeat approach:
- Client sends heartbeat every 5 seconds
- Marked offline after 30 seconds without heartbeat
- Last heartbeat time stored in Redis
Status change notifications:
- Notify user's friend list on change
- For users with many friends: only notify currently online friends
9.4 Netflix / Video Streaming Design
Video Upload Pipeline:
+-----------+ +-----------+ +-------------------+
| Uploader |--->| Object |--->| Transcoding |
| | | Store | | Pipeline |
+-----------+ | (S3) | | |
+-----------+ | +---------------+ |
| | 240p | |
| | 480p | |
| | 720p | |
| | 1080p | |
| | 4K | |
| +---------------+ |
+---------+----------+
v
+-------------------+
| Deploy to CDN |
| (Global Edges) |
+-------------------+
Adaptive Bitrate Streaming (ABR):
Automatically adjusts quality based on user network:
Bandwidth 10Mbps -> request 1080p segments
Bandwidth 3Mbps -> switch to 480p segments
Bandwidth recovers -> gradually increase quality
HLS (HTTP Live Streaming) approach:
1. Split video into 2-10 second segments
2. Encode each segment at multiple quality levels
3. Provide m3u8 manifest file listing segments
4. Client selects segments matching bandwidth
10. Back-of-Envelope Estimation
10.1 Commonly Used Numbers
Latency:
- L1 cache reference: ~1ns
- L2 cache reference: ~4ns
- Main memory reference: ~100ns
- SSD random read: ~150us
- HDD seek: ~10ms
- Same DC round trip: ~0.5ms
- US <-> Europe round trip: ~75ms
Capacity:
- 1 char = 1 byte (ASCII) / 2-4 bytes (UTF-8)
- 1 UUID = 36 bytes (string) / 16 bytes (binary)
- 1 image (medium) = ~200KB
- 1 video (1 min, 720p) = ~50MB
Daily conversion:
- 1 day = 86,400 sec = ~100,000 sec (simplified)
- 1 million requests/day = ~12 QPS
- 100 million requests/day = ~1,200 QPS
10.2 QPS Calculation Example
Twitter-scale estimation:
- DAU: 300 million
- Avg tweet views: 100/day
- Read QPS = 300M * 100 / 86,400 = ~347,000 QPS
- Peak QPS = 347,000 * 3 = ~1,000,000 QPS
- Avg tweets written: 2/day
- Write QPS = 300M * 2 / 86,400 = ~6,944 QPS
Storage:
- Tweet size: ~300 bytes (text + metadata)
- Daily: 600M tweets * 300B = ~180GB/day
- 5 years: 180GB * 365 * 5 = ~328TB
- With media: ~5x = ~1.6PB
11. Common Mistakes and Tips
11.1 Mistakes to Avoid
1. Jumping into design without clarifying requirements
-> Always invest 5 minutes in questions
2. Focusing on a single component
-> Big picture first, then deep dive where interviewer asks
3. Pursuing a perfect design
-> Explaining tradeoffs is more important
4. Design without numbers
-> Always calculate QPS, storage, bandwidth
5. Talking without interacting
-> Converse with interviewer, accept feedback
6. Over-engineering
-> Choose appropriate technology for the problem
11.2 Interview Tips
1. Structured Approach:
- Always follow the 4-step framework
- Time management in each step
2. Explain Tradeoffs:
- "The advantage of this approach is A, the drawback is B."
- "Alternative C exists, but D is more suitable for our situation."
3. Reference Real Examples:
- "Netflix solved this problem using X approach."
- "I experienced a similar issue in a previous project."
4. Whiteboard Usage:
- Clean diagrams
- Data flow arrows
- Numbers/metrics displayed
5. Accept Interviewer Hints:
- Interviewer questions are hints
- "Great point, if we improve that part..."
12. Quiz
Q1. Scalability
Explain 3 key differences between horizontal and vertical scaling.
Answer:
- Cost Structure: Vertical scaling costs increase exponentially with high-spec hardware, while horizontal scaling costs grow linearly with commodity machines.
- Fault Isolation: Vertical scaling is a single point of failure (SPOF), while horizontal scaling means one server failure does not affect the whole system.
- Scaling Limits: Vertical scaling has physical limits, while horizontal scaling is theoretically unlimited.
The tradeoff for horizontal scaling is increased complexity in data consistency management, session handling, and operations.
Q2. CAP Theorem
Why do banking systems choose CP? What problems arise if they choose AP?
Answer: Banking systems choose CP because balance consistency is absolutely critical. If AP is chosen, during a network partition, withdrawals could happen simultaneously on two nodes, allowing more money to be withdrawn than the actual balance -- a double spending problem. This directly leads to financial loss, so consistency must be guaranteed even at the cost of some availability.
Q3. Caching
What is a Cache Stampede and how do you prevent it?
Answer: A Cache Stampede occurs when the TTL of a popular cache entry expires and thousands of requests simultaneously flood the database.
Prevention methods:
- Mutex/Distributed Lock: Only the first request fetches from DB, others wait
- Pre-refresh: Refresh in background before TTL expiration (at 80% of TTL)
- TTL Randomization: Add small random values to prevent simultaneous expiration
- External refresh: Dedicated worker periodically refreshes the cache
Q4. Message Queues
When should you choose Kafka vs RabbitMQ?
Answer: Choose Kafka:
- Event streaming (log collection, real-time analytics)
- High throughput needed (millions of messages per second)
- Message replay needed (retained after consumption)
- Event sourcing patterns
Choose RabbitMQ:
- Traditional task queues (email sending, image processing)
- Complex routing needed (Exchange, Routing Key)
- Message priority needed
- RPC patterns
- Per-message ACK needed
Key difference: Kafka is an "event log", RabbitMQ is a "message broker".
Q5. Real Design
Why use Base62 encoding in a URL Shortener and how do you handle hash collisions?
Answer: Why Base62:
- Uses only URL-safe characters (0-9, a-z, A-Z)
- 62^7 = ~3.5 trillion unique URLs possible
- Short strings represent large numbers
Collision Handling:
- Auto-increment ID + Base62: No collisions (recommended)
- MD5/SHA256 + first 7 chars: Append characters on collision
- Collision detection + retry: Check DB for duplicates, retry with different hash
- Bloom Filter: Quick existence check, regenerate on collision
In practice, Method 1 (using Snowflake ID in distributed environments) is most reliable.
References
- Alex Xu, "System Design Interview - An Insider's Guide" (2020)
- Alex Xu, "System Design Interview Volume 2" (2022)
- Martin Kleppmann, "Designing Data-Intensive Applications" (2017)
- Google SRE Book - https://sre.google/sre-book/table-of-contents/
- AWS Well-Architected Framework - https://aws.amazon.com/architecture/well-architected/
- Meta Engineering Blog - https://engineering.fb.com/
- Netflix Tech Blog - https://netflixtechblog.com/
- Uber Engineering Blog - https://eng.uber.com/
- Cassandra vs MongoDB vs DynamoDB - https://db-engines.com/en/system/Amazon+DynamoDB%3BCassandra%3BMongoDB
- Kafka Documentation - https://kafka.apache.org/documentation/
- Redis Documentation - https://redis.io/documentation
- Consistent Hashing Paper - Karger et al. (1997)
- Dynamo Paper - Amazon (2007)
- MapReduce Paper - Google (2004)