Mendalami komunikasi sinkron antar layanan: menghindari sinkron-chain, menentukan kapan REST vs gRPC, otentikasi service-to-service tanpa menyebar token user, dan menerapkan retry, timeout, serta circuit breaker untuk downstream yang lambat atau gagal

Tokokita kini punya banyak layanan yang tersambung melalui gateway (episode 10). Episode ini masuk lebih dalam ke salah satu dari dua gaya komunikasi yang kita pilih di episode 2: komunikasi sinkron — REST dan gRPC. Sinkron memang lebih intuitif daripada event-driven, tapi ia penuh jebakan yang khas sistem terdistribusi: latensi, chaining, dan kegagalan yang merambat.
Mengapa episode ini penting? Karena keputusan "downstream mana yang dipanggil, kapan, dan seberapa agresif melakukan retry" menentukan seberapa parah skenario kegagalan satu layanan bisa menghantam layanan lain. Sinkron yang tidak disiplin adalah sumber utama cascade failure.
Jangan biarkan request berjalan berantai: gateway → order → product → inventory → ... untuk satu permintaan sederhana. Setiap hop menambah latensi dan memperbesar kemungkinan gagal — chain paling lambat menentukan total. Di tokokita:
BURUK: gateway → order → cart → product → inventory (5 hop, salah satu lambat semua lambat)
BAIK: gateway parallel → order (get cart) & product (reserve) hanya 2 hopAturan praktisnya:
Promise.all) daripada serial saat data tidak saling bergantung.Kontrak REST per resource dengan versioning (URL /v1/ atau header Accept: application/vnd.tokokita.v2+json). Versioning bukan hiasan: ketika order-service berubah kontraknya, versi lama tetap melayani client yang belum migrasi. Semua kontrak didokumentasikan via zod schema di shared-types (episode 3) — harus selaras antara service dan gateway.
Idempotency-Key saat membuat resource (sudah kita terapkan di order-service).gRPC membawa tiga hal yang REST tidak miliki secara native: kontrak statis (protobuf), streaming, dan performa tinggi di internal network. Cocok untuk komunikasi internal yang high-QPS dan typed: checkout memvalidasi stok ke product-service puluhan ribu kali per detik.
syntax = "proto3";
package tokokita.product;
service ProductService {
rpc ReserveStock(ReserveRequest) returns (ReserveResponse);
rpc GetProduct(GetProductRequest) returns (Product);
}
message ReserveRequest {
string product_id = 1;
int32 qty = 2;
string order_id = 3;
}
message ReserveResponse {
bool ok = 1;
string reason = 2;
}Dari protobuf, Bun mendukung gRPC via @connectrpc/connect atau grpc-js. Kapan memilih mana:
| Kriteria | REST | gRPC |
|---|---|---|
| Client luar (browser/App) | Wajib | Tidak langsung (butuh gateway) |
| Internal high-QPS | Bisa, verbose | Lebih ringan & typed |
| Streaming data | SSE/WebSocket | Native bi-directional |
| Tooling & debugging | curl | grpcurl + reflection |
| Kontrak | OpenAPI/manual | Protobuf wajib |
Note
Aturan praktis tokokita: eksternal REST (client cukup dengan HTTP biasa), internal gRPC untuk jalur panas (reserve stok, cek product). Jangan gRPC semua-jadi-satu hanya karena "lebih canggih" — setiap teknologi dua protokol berarti dua cara debugging, monitoring, dan pengujian.
Jangan menyebar token user ke semua layanan. Token user adalah identitas pemilik request; layanan internal membutuhkan identitas layanan (machine identity). Dua pendekatan standar:
x-service-token berisi token rahasia bersama yang diputar berkala. Mudah, cukup untuk awal.Di tokokita sekarang, internal call memakai internal token; migrasi ke mTLS dilakukan saat masuk Kubernetes (episode 17-18).
Downstream yang lambat atau gagal jangan dibiarkan menggantung.
Setiap panggilan sinkron wajib punya timeout:
const pc = await fetchInternal(service, path, {
signal: AbortSignal.timeout(2000), // 2 detik
})Waktu total harus lebih pendek dari timeout HTTP client pemanggil. Kalau gateway timeout 10 detik, service yang dipanggil jangan diizinkan hang 30 detik.
Retry hanya aman untuk operasi idempoten (GET, atau POST ber-idempotency-key). Retry GET yang lambat itu biasa; retry POST "create" tanpa idempotency berbahaya. Tambahkan jitter agar retry tidak mengerumuni target bersamaan (thundering herd):
export function backoff(attempt: number) {
const base = Math.min(1000 * 2 ** attempt, 5000) // 1s, 2s, 4s, kap 5s
return base * (0.5 + Math.random() * 0.5) // jitter 50-100%
}Robohkan panggilan ke downstream yang sedang jelas-jelas rusak — JANGAN terus mencoba dan memperparah. Circuit breaker punya tiga state:
CLOSED (normal) → OPEN (setelah N gagal, langkah 5 detik)
↑ │
└───── HALF-OPEN ←───┘ (uji 1 request; sukses → CLOSED)Implementasi bisa pakai library (misal cockatiel) atau manual:
export class CircuitBreaker {
private failures = 0
private openedAt: number | null = null
constructor(
private threshold = 5,
private cooldownMs = 5000,
) {}
get state(): 'open' | 'half-open' | 'closed' {
if (this.openedAt !== null && Date.now() - this.openedAt > this.cooldownMs) return 'half-open'
return this.openedAt !== null ? 'open' : 'closed'
}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') throw new Error('circuit open — downstream unavailable')
try {
const result = await fn()
this.failures = 0
this.openedAt = null
return result
} catch (err) {
this.failures += 1
if (this.failures >= this.threshold) this.openedAt = Date.now()
throw err
}
}
}Dengan circuit breaker, saat payment-service down, order-service langsung mengembalikan fallback (misal "pembayaran sedang gangguan") tanpa membanjiri payment-service dengan request yang pasti gagal — contoh nyata pencegahan cascade failure.
Warning
Peringatan paling berharga di episode ini: jangan retry tanpa timeout, dan jangan retry tanpa idempotency. Kombinasi timeout=5s + retry 10x tanpa jitter adalah resep membawa seluruh cluster tumbang saat satu layanan melambat — karena request menumpuk di antrian HTTP sambil menunggu. Selalu pasang keduanya bersama circuit breaker di sekitar downstream dengan beban tinggi.
Episode 11 membedah komunikasi sinkron:
Di episode 12 selanjutnya, giliran sisi kedua komunikasi: event-driven — Kafka & outbox pattern — memahami topic, partition, consumer group, offset, dan at-least-once, plus transactional outbox untuk memastikan event tidak hilang di sela-sela transaksi database. Sampai jumpa di episode 12!