Microservices adalah cara membangun aplikasi sebagai beberapa service kecil yang berdiri sendiri: tiap service punya kode, proses, dan datanya sendiri, di-deploy sendiri, dan bicara dengan service lain lewat jaringan. Lawannya monolith: satu aplikasi, satu proses, satu deploy. Bayangin bedanya warung dan food court. Di warung, satu dapur memasak semua pesananmu, satu kasir, satu stok. Di food court, nasi, sate, dan es teh datang dari tiga stan berbeda, masing-masing dengan dapur dan stoknya sendiri, dan pelayan mengantarkan pesananmu ke stan yang tepat. Pesananmu baru lengkap saat stan paling lambat selesai, dan kalau stan sate tutup, sebagian pesananmu gagal. Dua sifat itu yang paling sering dilupakan orang saat memecah aplikasi.
🧪 Tebakan cepat: catatan-api dipecah jadi empat service. Halaman satu catatan memanggil auth (15 ms), catatan (30 ms), lampiran (40 ms), dan profil (20 ms) berurutan, dan tiap panggilan menambah 2 ms di jaringan. Suatu hari lampiran-svc melambat jadi 1.200 ms. Berapa lama halaman catatan sekarang? Tebak dulu, jawabannya ada di bagian "Satu service lambat, seluruh request lambat".
Monolith: catatan-api hari ini
catatan-api adalah monolith. Satu proses Express di web-1 berisi semua modul: login, catatan, lampiran, ekspor PDF. Semuanya memakai satu database Postgres catatan. Halaman satu catatan dikerjakan di dalam proses yang sama: tiga function dipanggil, satu query JOIN ke Postgres, selesai sekitar 12 ms.
- Kelebihannya: satu repo, satu deploy, satu log. Memanggil modul lain cuma memanggil function, jauh di bawah satu milidetik.
JOINdan transaksi database bekerja seperti biasa. - Kekurangannya: semua bagian di-scale bersama, jadi ekspor PDF yang berat memaksa seluruh app dapat server lebih besar. Bug yang membuat proses crash mematikan semuanya. Dan saat timnya besar, semua orang mengubah repo dan jadwal deploy yang sama.
Kalau dipecah: satu request jadi banyak panggilan
Versi microservices catatan-api punya API gateway di depan, pintu masuk tunggal yang meneruskan request ke service yang tepat, dan empat service di belakangnya. Halaman GET /catatan/12 sekarang butuh empat panggilan jaringan:
| Urutan | Panggilan | Latency | + jaringan | Selesai di |
|---|---|---|---|---|
| 1 | auth-svc cek token |
15 ms | 2 ms | 17 ms |
| 2 | catatan-svc ambil catatan 12 |
30 ms | 2 ms | 49 ms |
| 3 | lampiran-svc daftar lampiran |
40 ms | 2 ms | 91 ms |
| 4 | profil-svc profil penulis |
20 ms | 2 ms | 113 ms |
113 ms, padahal di monolith 12 ms. Tiap panggilan juga bisa gagal sendiri: timeout, service sedang deploy, atau jaringan putus.
Satu service lambat, seluruh request lambat
Dengan panggilan berurutan, total waktu adalah jumlah semua panggilan. Kalau lampiran-svc melambat jadi 1.200 ms, halamannya jadi 17 + 32 + 1.202 + 22 = 1.273 ms. Itu jawaban tebakannya: satu service lambat menyeret seluruh request.
Setelah auth, tiga panggilan lain gak saling menunggu, jadi bisa dijalankan paralel dengan Promise.all. Totalnya jadi auth ditambah panggilan paling lambat:
| lampiran-svc | Berurutan | Paralel |
|---|---|---|
| 40 ms (normal) | 113 ms | 17 + 42 = 59 ms |
| 1.200 ms (lambat) | 1.273 ms | 17 + 1.202 = 1.219 ms |
Paralel membantu saat semuanya normal, tapi gak menyelamatkan saat satu service lambat: request tetap menunggu yang paling lambat. Makin banyak service yang disentuh satu request, makin sering ada yang lambat. Kalau tiap service lambat 1% dari waktu, request yang menyentuh 4 service kena lambat 1 - 0,99⁴ ≈ 3,9% dari waktu.
Timeout, fallback, dan retry
Obatnya: jangan menunggu selamanya. Beri tiap panggilan timeout, batas waktu menunggu, dan siapkan fallback, jawaban cadangan kalau batasnya lewat. Ini inti kode di Lab (service-nya palsu, latency-nya sama dengan tabel):
const LATENCY = { auth: 15, catatan: 30, lampiran: 1200, profil: 20 };
const HOP = 2;
function panggil(service) {
// Di production: fetch ke http://lampiran-svc/... dengan AbortSignal.timeout
return new Promise((resolve) => setTimeout(() => resolve({ service }), LATENCY[service] + HOP));
}
function denganTimeout(service, ms, cadangan) {
const batas = new Promise((resolve) => setTimeout(() => resolve(cadangan), ms));
return Promise.race([panggil(service), batas]);
}
async function halamanCatatan() {
const user = await panggil("auth");
const [catatan, lampiran, profil] = await Promise.all([
panggil("catatan"),
denganTimeout("lampiran", 300, { fallback: [] }),
panggil("profil"),
]);
return { user, catatan, lampiran, profil };
}
Promise.racemengembalikan yang selesai duluan: jawaban lampiran-svc, atau cadangan setelah 300 ms.- Hasilnya 17 + 300 = 317 ms. Halaman tampil dengan catatan dan profil, tanpa daftar lampiran, dan menulis "lampiran gagal dimuat". Ini disebut graceful degradation: sebagian fitur hilang, halamannya tetap jalan.
fetchbawaan Node baru menyerah setelah sekitar 5 menit menunggu header. Tanpa timeout sendiri, satu service macet bisa menahan request selama itu.
Retry (mencoba lagi) terdengar aman, tapi hati-hati. Dengan 2 retry, tiap halaman menunggu 17 + 3 × 300 = 917 ms, dan lampiran-svc yang sudah kewalahan menerima 3 kali lipat request. Retry hanya untuk request yang aman diulang (GET, atau yang idempotent), dengan jumlah terbatas dan jeda yang makin panjang (backoff).
Data milik masing-masing service
Di microservices, tiap service memiliki datanya sendiri. Tabel lampiran cuma boleh dibaca dan ditulis lampiran-svc. catatan-svc yang butuh lampiran harus bertanya lewat API, gak boleh membaca database lampiran langsung.
JOIN catatandenganlampiranhilang, diganti dua panggilan jaringan.- Menghapus catatan sekaligus lampirannya gak bisa lagi dalam satu transaksi database. Biasanya
catatan-svcmengumumkan event "catatan.dihapus", danlampiran-svcmembersihkan lampirannya sendiri. - Kalau semua service tetap berbagi satu database, mengubah satu tabel memaksa beberapa service deploy bersamaan. Itu disebut distributed monolith: repot microservices, tanpa kebebasannya.
Cara service berkomunikasi
- Synchronous (REST atau gRPC): service memanggil service lain lalu menunggu jawabannya, seperti
panggil()di atas. Sederhana, tapi pemanggil ikut lambat atau gagal kalau yang dipanggil bermasalah. - Asynchronous (message queue seperti RabbitMQ atau Kafka): service menaruh pesan di antrean lalu lanjut bekerja, dan service lain memprosesnya nanti. Cocok untuk "catatan.dihapus" atau "kirim email", karena pengguna gak perlu menunggu.
API gateway: satu pintu masuk
Gateway menyimpan tabel prefix path dan alamat service. Untuk tiap request, dia memilih prefix terpanjang yang cocok, lalu meneruskan sisa path-nya:
| Request | Prefix yang cocok | Diteruskan ke |
|---|---|---|
/api/orders/123 |
/api/orders (bukan /api, karena lebih panjang) |
http://orders:3002 dengan path /123 |
/auth/login |
/auth |
http://auth:3001 dengan path /login |
/unknown |
gak ada | error no route |
/authors |
gak ada: /auth cuma cocok sampai batas segmen |
error no route |
Prefix dicocokkan per segmen path: /auth menangkap /auth dan /auth/login, tapi bukan /authors, jadi cek path === prefix atau path.startsWith(prefix + "/"), bukan startsWith(prefix) saja. Itu persis tugas latihan routeRequest(gateway, path): kembalikan { service, path } dari prefix terpanjang, atau { error: "no route" }.
Kapan memecah, kapan jangan
Hampir semua sistem microservices yang sukses dimulai sebagai monolith (monolith first). Batas antar service baru kelihatan setelah aplikasinya dipakai, dan memindahkan batas yang salah jauh lebih mahal di microservices. Pecah kalau ada alasan nyata: tim sudah banyak dan saling menunggu untuk deploy, satu bagian butuh scaling yang sangat beda, atau domain-nya sudah jelas terpisah.
Untuk catatan-api dengan tim kecil dan satu server, keputusannya: jangan pecah. Rapikan monolith-nya per modul, dan kalau ekspor PDF mulai berat, pindahkan ke background worker lewat antrean. Pembahasan desainnya lebih dalam ada di track System Design, pelajaran Microservices vs Monolith.
⚠️ Jebakan yang sering ketemu
- Panggilan tanpa timeout. Satu service macet menahan semua request yang menunggunya, sampai server pemanggil kehabisan koneksi.
- Retry tanpa batas dan tanpa backoff. Service yang lambat dihujani request ulangan dan makin lambat. Retry hanya untuk request yang aman diulang.
- Berbagi satu database. Hasilnya distributed monolith. Tiap service memiliki datanya, yang lain bertanya lewat API atau event.
- Memecah terlalu awal. Aplikasi kecil dengan tim kecil membayar jaringan, monitoring, dan deploy berlipat tanpa dapat apa-apa.
TL;DR: Microservices memecah aplikasi jadi service kecil yang independen, masing-masing dengan kode, proses, dan datanya sendiri, yang berkomunikasi lewat REST, gRPC, atau message queue. Harganya: satu request jadi banyak panggilan jaringan, dan request selalu menunggu service paling lambat. Di Lab, lampiran-svc 1.200 ms membuat halaman 1.273 ms berurutan, 1.219 ms paralel, dan 317 ms dengan timeout 300 ms plus fallback. Retry melipatgandakan beban, jadi batasi dan beri backoff. Tiap service memiliki datanya; berbagi database = distributed monolith. API gateway memilih prefix terpanjang dan meneruskan sisa path. Mulai dari monolith, pecah hanya saat tim, scaling, atau domain benar-benar menuntutnya; catatan-api belum perlu.