Kita sudah membangun aplikasi monolith di Section Spring Framework: satu aplikasi, satu proses JVM, seluruh fitur dari autentikasi sampai laporan hidup dalam satu berkas JAR. Pertanyaan yang menghantui setiap tim saat aplikasi membesar adalah: apakah sistem perlu dipecah menjadi banyak service yang berjalan terpisah? Jawaban jujurnya bukan “ya karena itu arah modern”, melainkan “tergantung konsekuensi operasional yang sanggup ditanggung tim”. Microservices menukar kesederhanaan proses tunggal dengan kebebasan deploy dan skalabilitas per bagian. Chapter pembuka section ini membandingkan kedua gaya lewat satu alur bisnis yang sama, lalu memetakan kapan pecah dan kapan bertahan masuk akal.
Satu Alur Bisnis Dua Bentuk Penampungan #
Bentuk arsitektur tidak mengubah logika bisnis — Order tetap mencatat pesanan, Inventori tetap mengurangi stok. Yang berubah adalah wadah tempat kode itu tinggal dan media penghubung antarbagian. Program berikut menjalankan alur yang sama dua kali: pertama sebagai monolith (semua panggilan berupa method dalam satu proses), kedua sebagai microservices (setiap panggilan lintas batas menjadi lompatan jaringan):
import java.util.ArrayList;
import java.util.List;
/** Satu alur bisnis yang sama, dijalankan pada dua gaya arsitektur berbeda. */
public class DuaGayaArsitektur {
/** Satu langkah eksekusi: siapa pelakunya, apa aksinya, lintas jaringan atau tidak. */
record Langkah(String pelaku, String aksi, boolean lintasJaringan) {}
public static void main(String[] args) {
System.out.println("== MONOLITH: satu proses, satu unit deployable ==");
List<Langkah> mono = jalurMonolith();
cetak(mono);
System.out.println();
System.out.println("== MICROSERVICES: empat proses, empat unit deployable ==");
List<Langkah> mikro = jalurMicroservices();
cetak(mikro);
System.out.println();
System.out.println("== PERBANDINGAN ==");
System.out.println("unit deploy : monolith=1, microservices=4");
System.out.println("lompatan jaringan : monolith=" + hitungJaringan(mono)
+ ", microservices=" + hitungJaringan(mikro));
System.out.println("dampak bila modul Inventori berhenti:");
System.out.println(" monolith = seluruh proses ikut berhenti");
System.out.println(" microservices = 3 service lain tetap hidup");
}
static void cetak(List<Langkah> langkah) {
for (Langkah l : langkah) {
String media = l.lintasJaringan() ? "jaringan" : "method";
System.out.println(" [" + l.pelaku() + "] " + l.aksi() + " (" + media + ")");
}
}
static int hitungJaringan(List<Langkah> langkah) {
int total = 0;
for (Langkah l : langkah) {
if (l.lintasJaringan()) {
total++;
}
}
return total;
}
/** Pada monolith, empat modul tinggal dalam satu proses: cukup panggilan method. */
static List<Langkah> jalurMonolith() {
List<Langkah> jejak = new ArrayList<>();
jejak.add(new Langkah("proses Aplikasi", "Order.catat(pesanan-1001)", false));
jejak.add(new Langkah("proses Aplikasi", "Inventori.kurangiStok(pesanan-1001)", false));
jejak.add(new Langkah("proses Aplikasi", "Pembayaran.tagih(pesanan-1001)", false));
jejak.add(new Langkah("proses Aplikasi", "Notifikasi.kirim(pesanan-1001)", false));
return jejak;
}
/** Pada microservices, tiap modul jadi service dengan alamat sendiri. */
static List<Langkah> jalurMicroservices() {
List<Langkah> jejak = new ArrayList<>();
jejak.add(new Langkah("klien ", "POST order/api/pesanan", true));
jejak.add(new Langkah("svc order :8081", "catat pesanan-1001", false));
jejak.add(new Langkah("svc order :8081", "GET inventori/api/stok", true));
jejak.add(new Langkah("svc inventori :8082", "kurangiStok pesanan-1001", false));
jejak.add(new Langkah("svc order :8081", "POST pembayaran/api/tagih", true));
jejak.add(new Langkah("svc pembayaran :8083", "tagih pesanan-1001", false));
jejak.add(new Langkah("svc order :8081", "POST notifikasi/api/kirim", true));
jejak.add(new Langkah("svc notifikasi :8084", "kirim pesanan-1001", false));
return jejak;
}
}Output yang Dihasilkan:
== MONOLITH: satu proses, satu unit deployable ==
[proses Aplikasi] Order.catat(pesanan-1001) (method)
[proses Aplikasi] Inventori.kurangiStok(pesanan-1001) (method)
[proses Aplikasi] Pembayaran.tagih(pesanan-1001) (method)
[proses Aplikasi] Notifikasi.kirim(pesanan-1001) (method)
== MICROSERVICES: empat proses, empat unit deployable ==
[klien ] POST order/api/pesanan (jaringan)
[svc order :8081] catat pesanan-1001 (method)
[svc order :8081] GET inventori/api/stok (jaringan)
[svc inventori :8082] kurangiStok pesanan-1001 (method)
[svc order :8081] POST pembayaran/api/tagih (jaringan)
[svc pembayaran :8083] tagih pesanan-1001 (method)
[svc order :8081] POST notifikasi/api/kirim (jaringan)
[svc notifikasi :8084] kirim pesanan-1001 (method)
== PERBANDINGAN ==
unit deploy : monolith=1, microservices=4
lompatan jaringan : monolith=0, microservices=4
dampak bila modul Inventori berhenti:
monolith = seluruh proses ikut berhenti
microservices = 3 service lain tetap hidupMari kita bedah struktur programnya:
record Langkah(...): satu langkah eksekusi dicatat sebagai data — siapa pelakunya (pelaku), apa aksinya (aksi), dan apakah langkah tersebut menyeberangi jaringan (lintasJaringan). Merekam eksekusi sebagai daftar nilai seperti ini persis cara kerja distributed tracing yang akan kita bangun pada chapter Observability dan Tracing.jalurMonolith(): empat langkah seluruhnya bernilailintasJaringan = false. Dalam satu proses JVM, pemanggilanInventori.kurangiStok(...)hanya lompatan method — cepat, dan kegagalan apa pun berupa exception yang bisa ditangkaptry-catchdi tempat yang sama.jalurMicroservices(): serviceordermenjadi koordinator. Setiap kaliordermembutuhkan modul lain, program mencatat barisPOSTatauGETke alamat service tersebut — empat lompatan jaringan untuk alur yang di monolith selesai tanpa satu pun.hitungJaringan(...): menghitung langkah bertandatrue. Angka ini adalah biaya struktural microservices: setiap lompatan menambah latensi, titik kegagalan, dan kebutuhan penanganan error yang tidak lagi berupa exception lokal.
Perhatikan pula bagian dampak bila modul Inventori berhenti. Pada monolith, satu modul yang berhenti membawa seluruh proses — itulah blast radius maksimal. Pada microservices, tiga service lain tetap hidup; pengguna masih bisa menjelajah katalog meski pemesanan sedang tidak bisa diproses.
Konsekuensi Operasional #
Perbedaan pada satu alur tadi menjelma menjadi perbedaan besar di tingkat operasional. Tabel berikut merangkum keduanya berdampingan:
| Aspek | Monolith | Microservices |
|---|---|---|
| Unit deploy | satu JAR/WAR, sekali rilis | banyak service, rilis mandiri per tim |
| Skalabilitas | seluruh aplikasi direplikasi | hanya service yang sibuk direplikasi |
| Blast radius kegagalan | satu bagian berhenti, semua terdampak | kegagalan terkurung di satu service |
| Konsistensi data | satu transaksi ACID lokal | butuh saga dan konsistensi eventual |
| Komunikasi antarbagian | panggilan method | REST/messaging lewat jaringan |
| Pengujian | satu proses, integrasi mudah | butuh kontrak antar tim dan stub |
| Kebutuhan infrastruktur | sederhana: satu server aplikasi | registry, broker, observability, pipeline per service |
| Kendala teknologi | satu tumpukan untuk semua | tiap service boleh beda tumpukan |
Dua baris terakhir adalah daya tarik sekaligus jebakan. Kebebasan memilih teknologi per service hanya bernilai bila tim memang punya alasan berbeda-beda; tanpa alasan, kebebasan itu berubah menjadi perawatan empat tumpukan sekaligus.
Monolith yang Modular Dulu #
Martin Fowler menyarankan pendekatan monolith first: bangun dulu sebagai monolith yang bersih disiplin modulnya, karena batas domain biasanya baru terlihat jelas setelah sistem dipakai. Java mendukung jalur ini secara native — JPMS (Module System) yang kita pelajari di Section Packages dan Modules memaksa dependensi antarpaket dinyatakan eksplisit lewat requires, sehingga monolith modular punya dinding yang nanti bisa dipotong menjadi batas service tanpa nebak-nebak.
Kapan Pecah Mulai Masuk Akal #
Sinyal bahwa pemecahan mulai membayar biayanya: beberapa tim developer bertumbuk pada satu rilis karena satu berkas deployable; satu modul butuh skala atau tumpukan teknologi yang berbeda (misalnya modul pelaporan berat dibebankan CPU); atau area tertentu butuh siklus rilis jauh lebih cepat daripada sisanya. Bila sinyal-sinyal itu belum muncul, monolith modular tetap menjadi pilihan yang sah — banyak sistem besar dan sukses tetap berjalan sebagai monolith yang tertata.
Begitu keputusan memecah diambil, pertanyaan berikutnya segera menuntut jawaban presisi: di garis mana sistem dipotong? Chapter berikutnya membahas service boundaries — cara menemukan batas yang mengikuti kapabilitas bisnis, bukan lapisan teknis.
Referensi #
- Martin Fowler: Microservices
- Martin Fowler: Monolith First
- microservices.io: Pola Microservice Architecture
- microservices.io: Pola Monolithic Architecture
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
DuaGayaArsitektur.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
/** Satu alur bisnis yang sama, dijalankan pada dua gaya arsitektur berbeda. */
public class DuaGayaArsitektur {
/** Satu langkah eksekusi: siapa pelakunya, apa aksinya, lintas jaringan atau tidak. */
record Langkah(String pelaku, String aksi, boolean lintasJaringan) {}
public static void main(String[] args) {
System.out.println("== MONOLITH: satu proses, satu unit deployable ==");
List<Langkah> mono = jalurMonolith();
cetak(mono);
System.out.println();
System.out.println("== MICROSERVICES: empat proses, empat unit deployable ==");
List<Langkah> mikro = jalurMicroservices();
cetak(mikro);
System.out.println();
System.out.println("== PERBANDINGAN ==");
System.out.println("unit deploy : monolith=1, microservices=4");
System.out.println("lompatan jaringan : monolith=" + hitungJaringan(mono)
+ ", microservices=" + hitungJaringan(mikro));
System.out.println("dampak bila modul Inventori berhenti:");
System.out.println(" monolith = seluruh proses ikut berhenti");
System.out.println(" microservices = 3 service lain tetap hidup");
}
static void cetak(List<Langkah> langkah) {
for (Langkah l : langkah) {
String media = l.lintasJaringan() ? "jaringan" : "method";
System.out.println(" [" + l.pelaku() + "] " + l.aksi() + " (" + media + ")");
}
}
static int hitungJaringan(List<Langkah> langkah) {
int total = 0;
for (Langkah l : langkah) {
if (l.lintasJaringan()) {
total++;
}
}
return total;
}
/** Pada monolith, empat modul tinggal dalam satu proses: cukup panggilan method. */
static List<Langkah> jalurMonolith() {
List<Langkah> jejak = new ArrayList<>();
jejak.add(new Langkah("proses Aplikasi", "Order.catat(pesanan-1001)", false));
jejak.add(new Langkah("proses Aplikasi", "Inventori.kurangiStok(pesanan-1001)", false));
jejak.add(new Langkah("proses Aplikasi", "Pembayaran.tagih(pesanan-1001)", false));
jejak.add(new Langkah("proses Aplikasi", "Notifikasi.kirim(pesanan-1001)", false));
return jejak;
}
/** Pada microservices, tiap modul jadi service dengan alamat sendiri. */
static List<Langkah> jalurMicroservices() {
List<Langkah> jejak = new ArrayList<>();
jejak.add(new Langkah("klien ", "POST order/api/pesanan", true));
jejak.add(new Langkah("svc order :8081", "catat pesanan-1001", false));
jejak.add(new Langkah("svc order :8081", "GET inventori/api/stok", true));
jejak.add(new Langkah("svc inventori :8082", "kurangiStok pesanan-1001", false));
jejak.add(new Langkah("svc order :8081", "POST pembayaran/api/tagih", true));
jejak.add(new Langkah("svc pembayaran :8083", "tagih pesanan-1001", false));
jejak.add(new Langkah("svc order :8081", "POST notifikasi/api/kirim", true));
jejak.add(new Langkah("svc notifikasi :8084", "kirim pesanan-1001", false));
return jejak;
}
}