Tampilan
Kenapa penting
Capstone adalah tempat semua materi M13–M15 bertemu: dekomposisi service, data per service, saga, gateway, resilience, keamanan, tracing, Kubernetes, dan observability. Sebelum menulis kode, kamu menyusun dokumen desain — persis yang diminta di system design interview dan yang ditulis engineer senior sebelum proyek besar. Dokumen yang baik membuat keputusan eksplisit beserta alasannya, sehingga kamu (atau reviewer) bisa menantangnya sebelum biayanya mahal. Bab ini adalah desain yang akan kamu implementasikan di bab berikutnya.
Konsep inti
Kebutuhan fungsional dan non-fungsional
Fungsional (apa yang dilakukan sistem):
- Pengguna menelusuri dan mencari produk.
- Pengguna mengelola keranjang (tambah, ubah jumlah, hapus).
- Pengguna checkout: pesanan dibuat, stok di-reserve, pembayaran diproses.
- Pengguna menerima notifikasi status pesanan.
- Admin mengelola produk dan stok.
Non-fungsional (seberapa baik), dan harus berupa angka yang bisa diuji:
| Atribut | Target |
|---|---|
| Beban | 50.000 pesanan/hari, puncak 10x saat flash sale |
| Latency | GET /products p95 < 300 ms; POST /orders p95 < 800 ms |
| Ketersediaan | checkout 99,9% per 30 hari (SLO dari bab sebelumnya) |
| Konsistensi | stok tidak boleh oversell; data lain boleh eventual (detik) |
| Keamanan | OAuth2/JWT di gateway, data kartu tidak pernah disimpan |
| Operasional | deploy tanpa downtime, rollback < 5 menit |
Estimasi kasar (back-of-the-envelope) mengubah angka bisnis menjadi angka teknis:
java
public class Capacity {
public static void main(String[] args) {
long ordersPerDay = 50_000;
int peakFactor = 10; // flash sale: 10x rata-rata
double avgOrdersPerSec = ordersPerDay / 86_400.0;
double peakOrdersPerSec = avgOrdersPerSec * peakFactor;
int catalogReadsPerOrder = 40; // lihat-lihat sebelum checkout
System.out.printf("Checkout rata-rata: %.2f/detik%n", avgOrdersPerSec);
System.out.printf("Checkout puncak : %.1f/detik%n", peakOrdersPerSec);
System.out.printf("Katalog puncak : %.0f req/detik%n", peakOrdersPerSec * catalogReadsPerOrder);
long bytesPerOrder = 2_048;
System.out.printf("Data pesanan/tahun: %.1f GB%n", ordersPerDay * 365 * bytesPerOrder / 1e9);
}
}text
Checkout rata-rata: 0.58/detik
Checkout puncak : 5.8/detik
Katalog puncak : 231 req/detik
Data pesanan/tahun: 37.4 GBKesimpulannya: sistem ini read-heavy di katalog (perlu cache dan replika), sementara tulis checkout kecil tapi harus benar. Satu PostgreSQL per service sudah lebih dari cukup; tidak perlu sharding.
Dekomposisi service
Service dipotong berdasarkan kapabilitas bisnis dan kepemilikan data (M13), bukan berdasarkan tabel atau layer teknis.
| Service | Tanggung jawab | Data milik sendiri |
|---|---|---|
api-gateway | routing, validasi JWT, rate limit | — |
catalog-service | produk, harga, pencarian | PostgreSQL catalog + cache |
cart-service | keranjang per pengguna | Redis (TTL 7 hari) |
order-service | pesanan + orkestrator saga checkout | PostgreSQL orders + outbox |
payment-service | otorisasi pembayaran ke gateway eksternal | PostgreSQL payments |
inventory-service | stok dan reservasi | PostgreSQL inventory |
notification-service | email/push berdasarkan event | PostgreSQL notifications (log kirim) |
Tujuh service untuk satu developer itu banyak — dan itu disengaja untuk latihan. Di dunia nyata, tim kecil sering memulai dari modular monolith dan memecah hanya bagian yang benar-benar butuh skala atau deploy terpisah.
Kontrak API dan event utama
API sinkron (lewat gateway):
| Method & path | Service | Keterangan |
|---|---|---|
GET /products?q=&page= | catalog | cari & paginasi |
GET /products/{sku} | catalog | detail produk |
PUT /carts/me/items/{sku} | cart | set jumlah item |
POST /orders | order | checkout; header Idempotency-Key wajib; balas 202 Accepted |
GET /orders/{id} | order | status pesanan |
Event asinkron (Kafka, satu topic per aggregate, key = orderId agar urutan per pesanan terjaga):
| Event / command | Produsen | Konsumen |
|---|---|---|
OrderCreated | order | notification |
ReserveStock → StockReserved / StockRejected | order → inventory → order | saga |
AuthorizePayment → PaymentAuthorized / PaymentFailed | order → payment → order | saga |
ReleaseStock (kompensasi) | order | inventory |
OrderConfirmed / OrderCancelled | order | notification, catalog (statistik) |
ProductPriceChanged | catalog | cart (salinan harga lokal) |
Contoh payload event — selalu punya eventId untuk idempotensi dan occurredAt:
json
{
"eventId": "8f1c2a7e-4b0d-4c61-9a52-3d7e1f0b9c11",
"type": "StockReserved",
"orderId": "ORD-2026-000123",
"items": [{ "sku": "SUSU-1L", "quantity": 2 }],
"occurredAt": "2026-10-23T14:05:11Z"
}Alur checkout (saga orkestrasi)
text
Klien ──POST /orders──▶ Gateway ──▶ order-service
│ simpan Order(PENDING) + outbox ReserveStock [1 transaksi]
▼
inventory-service ── reserve stok ──▶ StockReserved
│ │
StockRejected ──▶ Order(CANCELLED) ▼
order-service ──▶ AuthorizePayment
│
payment-service ── otorisasi ke PSP
│ │
PaymentFailed PaymentAuthorized
│ │
ReleaseStock (kompensasi) Order(CONFIRMED)
Order(CANCELLED) OrderConfirmed ──▶ notificationKlien menerima 202 Accepted dengan orderId, lalu memantau GET /orders/{id}. Setiap langkah punya batas waktu: bila StockReserved tidak datang dalam 30 detik, orkestrator membatalkan pesanan; reservasi stok juga kedaluwarsa sendiri (misalnya 15 menit) sebagai jaring pengaman.
Keputusan teknis (ADR mini)
| # | Keputusan | Alasan | Alternatif yang ditolak |
|---|---|---|---|
| 1 | Saga orkestrasi di order-service | alur checkout punya 3+ langkah dan kompensasi; status terpusat mudah di-debug | koreografi — alur tersebar di banyak consumer |
| 2 | Transactional outbox untuk semua event | hindari dual write DB + Kafka | publish langsung setelah commit |
| 3 | Kafka 4 (KRaft) sebagai broker | retensi, replay, urutan per key | RabbitMQ — replay lebih terbatas |
| 4 | Reservasi stok dengan UPDATE ... WHERE available >= ? | cegah oversell tanpa lock panjang | lock pesimistis selama saga |
| 5 | Redis untuk keranjang | data sementara, akses per key, TTL bawaan | PostgreSQL — overkill untuk data sekali pakai |
| 6 | Discovery via DNS Kubernetes | sudah ada di platform, tanpa komponen tambahan | Eureka |
Observability dan keamanan
- Observability: setiap service mengekspor metrik Micrometer (
ServiceMonitor), trace OTLP ke Collector, log JSON dengantraceId. Dashboard RED template untuk semua service, ditambah dashboard bisnis: pesanan per menit, rasioOrderCancelled, umur saga yang macet. SLO dan alert burn rate untuk katalog dan checkout. - Keamanan: gateway memvalidasi JWT dari authorization server; service internal memvalidasi ulang token (token relay, M14) dan memeriksa kepemilikan (
order.userId == token.sub). Secret di Kubernetes Secret, bukan di image. Data kartu hanya ditangani PSP —payment-servicemenyimpan token referensi, bukan nomor kartu. NetworkPolicy membatasi siapa yang boleh memanggilpayment-service.
Risiko dan trade-off
| Risiko | Mitigasi |
|---|---|
Konsistensi eventual membingungkan pengguna (status PENDING) | UI menampilkan status jelas; target saga selesai < 5 detik |
| Event ganda atau tidak berurutan | consumer idempoten (eventId disimpan), key = orderId |
| Saga macet karena service mati | timeout + alert "saga lebih tua dari 5 menit" |
| Kompleksitas operasional 7 service + Kafka | Helm chart seragam, dashboard template, runbook |
| PSP eksternal lambat | timeout, retry dengan idempotency key ke PSP, circuit breaker |
Rencana implementasi bertahap
- Walking skeleton: gateway +
order-service+inventory-service, checkout tanpa pembayaran, deploy ke kind, tracing jalan. - Saga lengkap:
payment-service(PSP palsu), kompensasi, outbox, consumer idempoten. - Katalog & keranjang:
catalog-servicedengan cache,cart-servicedi Redis, eventProductPriceChanged. - Notifikasi & keamanan:
notification-service, JWT end-to-end, NetworkPolicy. - Production readiness: SLO, alert, load test flash sale, runbook — bab capstone production.
Setiap tahap berakhir dengan sistem yang bisa di-deploy dan di-demo, bukan potongan setengah jadi.
Contoh di dunia backend
Potongan inti reservasi stok di inventory-service — satu pernyataan atomik yang menolak bila stok tidak cukup:
sql
-- Potongan: dijalankan per item dalam satu transaksi, lalu event StockReserved/StockRejected ditulis ke outbox
UPDATE inventory_item
SET available = available - :quantity,
reserved = reserved + :quantity
WHERE sku = :sku
AND available >= :quantity;
-- 0 baris terpengaruh → stok tidak cukup → rollback transaksi dan tulis StockRejectedDan tabel outbox milik order-service, yang dibaca relay untuk diterbitkan ke Kafka:
sql
-- Potongan skema (PostgreSQL)
CREATE TABLE outbox_event (
id UUID PRIMARY KEY,
aggregate_id VARCHAR(64) NOT NULL, -- orderId, dipakai sebagai key Kafka
type VARCHAR(100) NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ
);
CREATE INDEX idx_outbox_unpublished ON outbox_event (created_at) WHERE published_at IS NULL;Pertanyaan interview
1. Bagaimana kamu mencegah oversell saat flash sale?
Kurangi stok dengan pernyataan kondisional atomik (WHERE available >= :quantity) di service pemilik stok, sehingga dua request bersamaan tidak bisa sama-sama lolos. Reservasi punya kedaluwarsa, dan pembayaran gagal memicu kompensasi ReleaseStock. Untuk beban ekstrem, antrean per SKU atau token stok di Redis bisa ditambahkan, dengan biaya kompleksitas.
2. Kenapa POST /orders membalas 202 Accepted, bukan 201 Created dengan status final?
Karena hasil akhirnya bergantung pada saga asinkron (stok dan pembayaran). Membalas 202 dengan orderId membuat request cepat dan tidak terikat pada ketersediaan semua service; klien memantau status. Header Idempotency-Key mencegah pesanan ganda saat klien mengulang request.
3. Kapan kamu memilih koreografi daripada orkestrasi?
Koreografi cocok untuk reaksi sederhana yang independen, seperti notification-service yang mendengarkan OrderConfirmed. Untuk alur berurutan dengan kompensasi seperti checkout, orkestrasi lebih mudah dipahami, diuji, dan dipantau karena status saga ada di satu tempat.
Jebakan umum
- NFR tanpa angka ("harus cepat dan scalable") — tidak bisa diuji, tidak bisa dijadikan SLO.
- Service dipotong per tabel, sehingga satu fitur butuh panggilan sinkron berantai ke banyak service.
- Berbagi database antar-service "sementara" yang akhirnya permanen.
- Dual write database + Kafka tanpa outbox — event hilang diam-diam.
- Lupa idempotensi di consumer dan di
POST /orders; retry menghasilkan pesanan atau potongan stok ganda. - Membangun semua service sekaligus lalu mengintegrasikannya di akhir — risiko terbesar ditemukan paling lambat.
Ringkasan
- Dokumen desain dimulai dari kebutuhan fungsional dan NFR berangka, lalu estimasi kapasitas kasar.
- Service dipotong per kapabilitas bisnis dengan database sendiri; data lain diakses lewat API atau event.
- Kontrak API dan event ditulis eksplisit; event membawa
eventIddan keyorderId. - Checkout memakai saga orkestrasi dengan kompensasi, outbox, dan consumer idempoten.
- Keputusan dicatat sebagai ADR mini bersama alternatif, risiko, dan mitigasinya.
- Implementasi dimulai dari walking skeleton end-to-end, lalu ditebalkan bertahap.