1. Pengenalan System Design
System Design adalah proses mendefinisikan arsitektur, komponen, module, interface, dan data flow dari suatu sistem untuk memenuhi kebutuhan spesifik. Ini adalah skill krusial yang membedakan developer junior dari senior.
Bayangkan kamu diminta mendesain sistem seperti Twitter, Uber, atau Netflix. Tidak cukup hanya bisa coding β kamu harus memahami bagaimana sistem menangani jutaan pengguna, terabyte data, dan latency rendah.
Mengapa System Design Penting?
| Alasan | Penjelasan |
|---|---|
| Interview Teknis | 80% interview senior/staff di FAANG menguji system design |
| Karir Level Senior | Naik ke senior/staff membutuhkan kemampuan desain sistem |
| Produk Skala Besar | Sistem yang tidak scalable akan gagal saat user bertambah |
| Kolaborasi Tim | Memahami keseluruhan sistem membantu komunikasi antar tim |
| Decision Making | Membuat keputusan teknis yang tepat untuk trade-off |
βββββββββββ ββββββββββββββββ βββββββββββββββββββββββββββββββ
β Client ββββββΆβ CDN ββββββΆβ Load Balancer β
β (Browser/β β (Static β β (Nginx/HAProxy/ALB) β
β Mobile) β β Assets) β ββββββββββββ¬βββββββββββββββββββ
βββββββββββ ββββββββββββββββ β
β βββββββββββββββ
βββββββββββββΌββ€ App Server β
β β β (Node.js) β
β β ββββββββββββββββ
β β βββββββββββββββ
β βββ€ App Server β
β β (Node.js) β
β ββββββββ¬ββββββββ
β β
ββββββββββββ΄βββ βββββββββββββ΄βββββββββββ
β Cache β β Database β
β (Redis) β β βββββββββ βββββββββ β
β Cluster β β βPrimaryβ βReplicaβ β
βββββββββββββββ β βββββββββ βββββββββ β
ββββββββββββββββββββββββ
Saat menghadapi system design problem, gunakan framework ini: 1) Requirements (functional & non-functional), 2) Estimation (users, data, bandwidth), 3) High-level Design, 4) Detailed Design, 5) Trade-offs & Bottlenecks.
2. Scalability
Scalability adalah kemampuan sistem untuk menangani peningkatan beban tanpa menurunkan performa. Ada dua jenis scaling:
Vertical Scaling vs Horizontal Scaling
| Aspek | Vertical (Scale Up) | Horizontal (Scale Out) |
|---|---|---|
| Cara | Tambah resource ke 1 server (RAM, CPU) | Tambah server baru |
| Batas | Hardware limit | Hampir tidak terbatas |
| Kompleksitas | π’ Rendah | π‘ Tinggi (perlu load balancer, distributed state) |
| Cost | Naik eksponensial | Linear scaling |
| Single Point of Failure | β οΈ Ya | β Tidak (redundancy) |
| Cocok untuk | Startup kecil, database awal | Produksi skala besar, high availability |
Contoh: Scalability Calculation
# Asumsi: # - 200 juta DAU (Daily Active Users) # - Setiap user membaca 10 tweets/hari = 2 miliar reads/hari # - Setiap user membuat 2 tweets/hari = 400 juta writes/hari # Write per detik: # 400,000,000 / 86,400 β 4,629 writes/detik # Read per detik: # 2,000,000,000 / 86,400 β 23,148 reads/detik # Storage per hari (rata-rata tweet 300 bytes): # 400,000,000 Γ 300 bytes = 120 GB/hari = 43.8 TB/tahun # Bandwidth (read, 1 KB per tweet + metadata): # 23,148 reads/detik Γ 1 KB = ~23 MB/detik β 184 Mbps # Kesimpulan: # - Database perlu handle ~5K writes/detik β perlu sharding # - Cache sangat krusial untuk 23K reads/detik # - CDN untuk media (foto, video) # - Message queue untuk async fanout tweet ke followers
Micro vs Macro Scaling
- Database Replication β Primary untuk write, replica untuk read
- Database Sharding β Bagi data ke beberapa database berdasarkan key
- Service Decomposition β Pisahkan service berdasarkan domain (microservices)
- Async Processing β Gunakan message queue untuk operasi yang tidak perlu real-time
- Auto-scaling β Tambah/kurangi server otomatis berdasarkan load
3. Load Balancing
Load Balancer mendistribusikan traffic ke beberapa server secara merata. Ini adalah komponen kunci untuk high availability dan horizontal scaling.
Jenis Load Balancing
| Algoritma | Cara Kerja | Cocok Untuk |
|---|---|---|
| Round Robin | Request bergantian ke tiap server secara berurutan | Server dengan spec sama, request uniform |
| Weighted Round Robin | Seperti round robin tapi server dengan kapasitas lebih mendapat lebih banyak | Server dengan spec berbeda |
| Least Connections | Kirim ke server dengan koneksi aktif paling sedikit | Request dengan durasi bervariasi |
| IP Hash | Hash dari IP client menentukan server tujuan | Session affinity / sticky session |
| Consistent Hashing | Distribusi berdasarkan hash ring β minimal reshuffling saat scaling | Distributed cache, database sharding |
ββββββββββββββββββββββββ
β Load Balancer β
β (L4 / L7 layer) β
ββββββββββ¬ββββββββββββββ
β
ββββββββββββββββΌβββββββββββββββ
βΌ βΌ βΌ
ββββββββββββ ββββββββββββ ββββββββββββ
β Server 1 β β Server 2 β β Server 3 β
β (App) β β (App) β β (App) β
ββββββ¬ββββββ ββββββ¬ββββββ ββββββ¬ββββββ
β β β
ββββββββββββββββΌβββββββββββββββ
βΌ
ββββββββββββββββ
β Database β
β (Primary + β
β Replicas) β
ββββββββββββββββ
Layer 4 (Transport): Distribusi berdasarkan IP + port
Layer 7 (Application): Distribusi berdasarkan URL, headers, cookies
Health Check: Load balancer mengecek server secara periodik
Server yang down akan di-skip dari rotasi
4. Database Design & Scaling
Database sering menjadi bottleneck pertama saat sistem mulai scale. Memahami strategi scaling database sangat penting.
Database Scaling Strategies
1. Read Replicas
# Primary server (postgresql.conf)
wal_level = replica
max_wal_senders = 5
wal_keep_size = '1GB'
# Replica server (postgresql.conf)
primary_conninfo = 'host=primary-db port=5432 user=replicator'
hot_standby = on
# Aplikasi: routing query ke primary/replica
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL // Primary untuk write
}
}
});
// Untuk read-heavy operations, gunakan replica
const readPrisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_REPLICA_URL // Replica untuk read
}
}
});
// Write ke primary
await prisma.user.create({ data: { name: 'Budi' } });
// Read dari replica
const users = await readPrisma.user.findMany();
2. Database Sharding
| Strategy | Cara Kerja | Pro | Kontra |
|---|---|---|---|
| Range-based | Berdasarkan range ID/tanggal | Sederhana, range query mudah | Hotspot pada shard terbaru |
| Hash-based | Hash dari shard key | Distribusi merata | Range query sulit |
| Directory-based | Lookup table untuk mapping | Fleksibel | Single point of failure |
| Geo-based | Berdasarkan lokasi user | Latency rendah per region | Cross-region query kompleks |
3. CAP Theorem
Consistency
/\
/ \
/ \
/ PICK \
/ TWO \
/ββββββββββ\
/ \
Availability ββββ Partition
Tolerance
C (Consistency): Semua node melihat data yang sama
A (Availability): Setiap request mendapat response
P (Partition Tolerance): Sistem tetap jalan meski network partition
Contoh pilihan:
βββ CP (Consistency + Partition): MongoDB, HBase
β βββ Data konsisten tapi bisa reject request saat partition
βββ AP (Availability + Partition): Cassandra, DynamoDB
β βββ Selalu available tapi data bisa inconsistency sementara
βββ CA (Consistency + Availability): PostgreSQL (single node)
βββ Konsisten dan available, tapi tidak partition-tolerant
5. Caching Strategies
Caching adalah teknik menyimpan data yang sering diakses di tempat yang lebih cepat diakses. Ini salah satu optimasi paling powerful untuk mengurangi latency dan beban database.
Caching Levels
| Level | Teknologi | Latency | Use Case |
|---|---|---|---|
| Browser Cache | HTTP headers (Cache-Control) | ~0ms | Static assets, API responses |
| CDN Cache | Cloudflare, CloudFront | ~5ms | Static files, global distribution |
| Application Cache | In-memory (HashMap) | ~0.1ms | Config, computed values |
| Distributed Cache | Redis, Memcached | ~1ms | Session, user data, API results |
| Database Cache | Query cache, buffer pool | ~5ms | Frequent queries |
Caching Patterns
import Redis from 'ioredis';
const redis = new Redis();
// ============================================
// PATTERN 1: Cache-Aside (Lazy Loading)
// ============================================
async function getUserById(id: string) {
const cacheKey = `user:${id}`;
// 1. Check cache dulu
const cached = await redis.get(cacheKey);
if (cached) {
console.log('Cache HIT');
return JSON.parse(cached);
}
// 2. Cache miss β query database
console.log('Cache MISS');
const user = await db.user.findUnique({ where: { id } });
// 3. Simpan ke cache (TTL 5 menit)
if (user) {
await redis.setex(cacheKey, 300, JSON.stringify(user));
}
return user;
}
// ============================================
// PATTERN 2: Write-Through
// ============================================
async function updateUser(id: string, data: UserUpdate) {
// 1. Update database
const user = await db.user.update({ where: { id }, data });
// 2. Langsung update cache
await redis.setex(`user:${id}`, 300, JSON.stringify(user));
return user;
}
// ============================================
// PATTERN 3: Cache Invalidation
// ============================================
async function deleteUser(id: string) {
await db.user.delete({ where: { id } });
// Hapus dari cache
await redis.del(`user:${id}`);
}
// ============================================
// PATTERN 4: Cache Stampede Prevention
// ============================================
async function getPopularProducts() {
const cacheKey = 'popular:products';
const ttl = 3600; // 1 jam
let data = await redis.get(cacheKey);
if (data) return JSON.parse(data);
// Gunakan lock untuk hindari stampede
const lockKey = `${cacheKey}:lock`;
const locked = await redis.set(lockKey, '1', 'NX', 'EX', 30);
if (locked) {
const products = await db.product.findMany({
orderBy: { views: 'desc' },
take: 50
});
await redis.setex(cacheKey, ttl, JSON.stringify(products));
await redis.del(lockKey);
return products;
}
// Tunggu sebentar lalu coba dari cache lagi
await new Promise(r => setTimeout(r, 100));
return getPopularProducts();
}
Phil Butler pernah berkata: "There are only two hard things in Computer Science: cache invalidation and naming things." Pastikan kamu menetapkan TTL yang tepat, dan punya strategi invalidation yang jelas saat data berubah.
6. Message Queue & Async Processing
Message queue memungkinkan komunikasi async antar service. Alih-alih memanggil service lain secara langsung (synchronous), kamu mengirim pesan ke queue dan service lain memprosesnya kapan pun siap.
Kapan Menggunakan Message Queue?
| Use Case | Tanpa Queue | Dengan Queue |
|---|---|---|
| Email notification | User tunggu 3 detik saat checkout | Response instant, email dikirim async |
| Image processing | Upload timeout untuk file besar | Upload selesai, processing di background |
| Order processing | Satu kegagalan = semua gagal | Retry otomatis, processing terus walau ada error |
| Analytics logging | Write ke analytics di setiap request = lambat | Log ke queue, batch insert ke analytics DB |
import amqplib from 'amqplib';
// === PRODUCER: Mengirim pesan ke queue ===
async function sendOrderNotification(order: Order) {
const conn = await amqplib.connect('amqp://localhost');
const channel = await conn.createChannel();
const queue = 'order_notifications';
await channel.assertQueue(queue, { durable: true });
const message = {
type: 'ORDER_CREATED',
data: {
orderId: order.id,
userId: order.userId,
total: order.total,
timestamp: new Date().toISOString()
}
};
channel.sendToQueue(queue, Buffer.from(JSON.stringify(message)), {
persistent: true // Pesan tetap ada walau RabbitMQ restart
});
console.log('Order notification sent:', order.id);
await channel.close();
await conn.close();
}
// === CONSUMER: Memproses pesan dari queue ===
async function processOrderNotifications() {
const conn = await amqplib.connect('amqp://localhost');
const channel = await conn.createChannel();
const queue = 'order_notifications';
await channel.assertQueue(queue, { durable: true });
// Prefetch: proses 1 pesan sekaligus
channel.prefetch(1);
console.log('Waiting for order notifications...');
channel.consume(queue, async (msg) => {
if (!msg) return;
const { type, data } = JSON.parse(msg.content.toString());
try {
switch (type) {
case 'ORDER_CREATED':
await sendEmail(data.userId, 'Order confirmed!');
await updateAnalytics(data);
await notifyWarehouse(data.orderId);
break;
}
// Acknowledge: pesan berhasil diproses
channel.ack(msg);
} catch (error) {
// Reject & requeue: pesan akan diproses ulang
channel.nack(msg, false, true);
}
});
}
7. Microservices Architecture
Microservices adalah arsitektur di mana aplikasi dibagi menjadi service-service kecil yang independen, masing-masing bertanggung jawab atas satu fungsi bisnis.
Monolith vs Microservices
| Aspek | Monolith | Microservices |
|---|---|---|
| Deployment | Satu aplikasi, satu deploy | Per-service deploy independen |
| Scaling | Scale seluruh aplikasi | Scale per service sesuai beban |
| Technology | Satu tech stack | Bebas pilih tech stack per service |
| Team | Tim besar, satu codebase | Tim kecil, ownership jelas |
| Complexity | Sederhana di awal | Operational complexity tinggi |
| Communication | Function call | Network call (HTTP/gRPC/message queue) |
| Data | Satu database | Database per service |
| Cocok untuk | Startup, MVP, tim kecil | Produk mature, tim besar |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β API GATEWAY β
β (Authentication, Rate Limiting, Routing) β
ββββββββ¬ββββββββββββββ¬βββββββββββββββ¬βββββββββββββββ¬ββββββββββββ
β β β β
βΌ βΌ βΌ βΌ
ββββββββββββββ ββββββββββββ βββββββββββββ ββββββββββββββββ
β User β β Product β β Order β β Payment β
β Service β β Service β β Service β β Service β
β β β β β β β β
β - Register β β - Search β β - Create β β - Process β
β - Auth β β - CRUD β β - Status β β - Refund β
β - Profile β β - Stock β β - History β β - Invoice β
βββββββ¬βββββββ ββββββ¬ββββββ βββββββ¬ββββββ ββββββββ¬ββββββββ
β β β β
βΌ βΌ βΌ βΌ
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββββββ
βUser DB β βProduct DBβ βOrder DB β β Payment DB β
β(Postgres)β β(Postgres)β β(Postgres)β β (Postgres) β
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββββββ
Communication:
βββ Sync: REST/gRPC antar service
βββ Async: Message queue (RabbitMQ/Kafka)
βββ Events: Event-driven architecture
Banyak startup membuat kesalahan dengan langsung pakai microservices padahal belum perlu. Mulai dari modular monolith β pisahkan kode ke module yang jelas, dan baru pecah jadi microservices ketika benar-benar butuh (tim besar, service perlu scaling independen, tech stack berbeda).
8. Design Patterns untuk Distributed System
1. Circuit Breaker Pattern
enum CircuitState {
CLOSED = 'CLOSED', // Normal β request lewat
OPEN = 'OPEN', // Gagal terus β block request
HALF_OPEN = 'HALF_OPEN' // Testing β coba beberapa request
}
class CircuitBreaker {
private state = CircuitState.CLOSED;
private failureCount = 0;
private lastFailureTime = 0;
private readonly threshold = 5; // Max failures
private readonly timeout = 30000; // 30 detik cooldown
async call(fn: () => Promise): Promise {
if (this.state === CircuitState.OPEN) {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = CircuitState.HALF_OPEN;
} else {
throw new Error('Circuit is OPEN β service unavailable');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failureCount = 0;
this.state = CircuitState.CLOSED;
}
private onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.threshold) {
this.state = CircuitState.OPEN;
}
}
}
// Penggunaan
const paymentBreaker = new CircuitBreaker();
const result = await paymentBreaker.call(() =>
paymentService.processPayment(orderId, amount)
);
2. Rate Limiting Pattern
-- Redis Lua script: Token Bucket Rate Limiter
-- KEYS[1] = rate_limit:{user_id}
-- ARGV[1] = max_tokens (e.g., 100)
-- ARGV[2] = refill_rate (e.g., 10 per second)
-- ARGV[3] = current_timestamp
local key = KEYS[1]
local max_tokens = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local data = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(data[1]) or max_tokens
local last_refill = tonumber(data[2]) or now
-- Refill tokens
local elapsed = now - last_refill
local new_tokens = math.min(max_tokens, tokens + elapsed * refill_rate)
if new_tokens >= 1 then
-- Consume 1 token
redis.call('HMSET', key, 'tokens', new_tokens - 1, 'last_refill', now)
redis.call('EXPIRE', key, 3600)
return 1 -- Allowed
else
redis.call('HMSET', key, 'tokens', new_tokens, 'last_refill', now)
return 0 -- Rate limited
end
3. Other Important Patterns
| Pattern | Fungsi | Contoh Penggunaan |
|---|---|---|
| Saga Pattern | Distributed transactions across microservices | Order β Payment β Inventory β Shipping |
| CQRS | Pisahkan model read dan write | Read model dioptimasi untuk query, write model untuk konsistensi |
| Event Sourcing | Simpan semua perubahan sebagai event | Audit trail, financial transactions |
| Retry with Exponential Backoff | Retry failed request dengan delay meningkat | API calls ke service eksternal |
| Bulkhead | Isolasikan failure agar tidak menyebar | Thread pool per service dependency |
9. Case Study: Mendesain URL Shortener
Mari kita terapkan semua konsep di atas untuk mendesain sistem URL Shortener seperti bit.ly.
Step 1: Requirements
Functional:
- User bisa memendekkan URL panjang menjadi short URL
- User bisa redirect dari short URL ke URL asli
- Custom alias (opsional)
- Analytics: jumlah klik, referer, geolocation
Non-Functional:
- 100 juta URL baru per bulan
- Read:Write ratio = 100:1 (banyak redirect, sedikit create)
- Latency redirect < 100ms
- 99.99% availability
- URL tidak expired (atau custom TTL)
Step 2: High-Level Design
Client β Load Balancer β API Server
β
βββββββββββΌββββββββββ
βΌ βΌ βΌ
βββββββββββ ββββββββββ ββββββββββββ
β Cache β β URL DB β β Analytics β
β (Redis) β β(Postgresβ β Service β
β β β/DynamoDBβ β(Kafkaβ β
βββββββββββ ββββββββββ β Clickhouseβ
ββββββββββββ
Short URL Generation:
βββ Base62 encode dari auto-increment ID
βββ Atau hash (MD5/SHA256) β ambil 6-7 karakter pertama
βββ Contoh: bit.ly/a1B2c3
Read Flow (99.9% traffic):
1. User buka short URL
2. Check Redis cache β if HIT, redirect (301)
3. Cache MISS β query DB β set cache β redirect
Write Flow (0.1% traffic):
1. User submit long URL
2. Generate short code (unique)
3. Save to DB
4. Return short URL
Database Schema:
CREATE TABLE urls (
id BIGSERIAL PRIMARY KEY,
short_code VARCHAR(10) UNIQUE NOT NULL,
long_url TEXT NOT NULL,
user_id UUID,
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP,
click_count BIGINT DEFAULT 0
);
CREATE INDEX idx_short_code ON urls(short_code);
10. Quiz Pemahaman
Uji pemahaman kamu tentang system design: