Kesalahan pemotongan paling mahal dalam microservices bukan service yang terlalu besar, melainkan service yang terpotong di tempat yang salah. Batas yang mengikuti lapisan teknis — satu service untuk UI, satu untuk logika, satu untuk database — menghasilkan sistem yang secara teknis terdistribusi tetapi secara logika tetap satu benjolan: setiap fitur bisnis harus menyentuh ketiga service sekaligus. Batas yang sehat mengikuti cara bisnis memandang dirinya sendiri: katalog, pesanan, pengiriman, penagihan. Chapter ini membuktikan perbedaannya lewat program yang menghitung berapa service yang tersentuh oleh satu fitur dan satu alur — angka yang sama, dua hasil berlawanan.
Masalah Potongan Lapisan Teknis #
Program berikut menyimpan dua dekomposisi dari sistem toko daring yang persis sama isinya — sembilan tanggung jawab, tiga domain. Yang berbeda hanya cara tanggung jawab tersebut dibagi ke dalam service:
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/** Membandingkan dua cara memotong sistem yang sama menjadi service. */
public class BatasKapabilitas {
/** Satu service beserta daftar tanggung jawab yang ditampungnya. */
record Layanan(String nama, List<String> isi) {}
// Alur "lihat produk": tiga kebutuhan pada domain katalog
static final List<String> ALUR_LIHAT_PRODUK =
List.of("ui katalog", "logika katalog", "data katalog");
// Fitur baru "voucher": tiga kebutuhan pada domain pesanan
static final List<String> FITUR_VOUCHER =
List.of("ui pesanan", "logika pesanan", "data pesanan");
public static void main(String[] args) {
// Potongan A: batas mengikuti lapisan teknis (ui / logika / data)
List<Layanan> perLapisan = List.of(
new Layanan("svc-ui", List.of(
"ui katalog", "ui pesanan", "ui pengiriman")),
new Layanan("svc-logika", List.of(
"logika katalog", "logika pesanan", "logika pengiriman")),
new Layanan("svc-data", List.of(
"data katalog", "data pesanan", "data pengiriman")));
// Potongan B: batas mengikuti kapabilitas bisnis (katalog / pesanan / pengiriman)
List<Layanan> perKapabilitas = List.of(
new Layanan("svc-katalog", List.of(
"ui katalog", "logika katalog", "data katalog")),
new Layanan("svc-pesanan", List.of(
"ui pesanan", "logika pesanan", "data pesanan")),
new Layanan("svc-pengiriman", List.of(
"ui pengiriman", "logika pengiriman", "data pengiriman")));
System.out.println("== POTONGAN PER LAPISAN TEKNIS ==");
tampilkan(perLapisan);
laporan(perLapisan);
System.out.println();
System.out.println("== POTONGAN PER KAPABILITAS BISNIS ==");
tampilkan(perKapabilitas);
laporan(perKapabilitas);
}
static void tampilkan(List<Layanan> daftar) {
for (Layanan l : daftar) {
System.out.println(" " + String.format("%-16s", l.nama()) + ": "
+ String.join(", ", l.isi()));
}
}
static void laporan(List<Layanan> daftar) {
Set<String> voucher = penampung(daftar, FITUR_VOUCHER);
Set<String> lihatProduk = penampung(daftar, ALUR_LIHAT_PRODUK);
System.out.println(" fitur \"voucher\" dikerjakan oleh "
+ voucher.size() + " service: " + voucher);
System.out.println(" alur \"lihat produk\" melewati "
+ lihatProduk.size() + " service: " + lihatProduk);
}
/** Kumpulan service yang menampung minimal satu kebutuhan pada daftar. */
static Set<String> penampung(List<Layanan> daftar, List<String> kebutuhan) {
Set<String> hasil = new TreeSet<>();
for (Layanan l : daftar) {
for (String k : kebutuhan) {
if (l.isi().contains(k)) {
hasil.add(l.nama());
}
}
}
return hasil;
}
}Output yang Dihasilkan:
== POTONGAN PER LAPISAN TEKNIS ==
svc-ui : ui katalog, ui pesanan, ui pengiriman
svc-logika : logika katalog, logika pesanan, logika pengiriman
svc-data : data katalog, data pesanan, data pengiriman
fitur "voucher" dikerjakan oleh 3 service: [svc-data, svc-logika, svc-ui]
alur "lihat produk" melewati 3 service: [svc-data, svc-logika, svc-ui]
== POTONGAN PER KAPABILITAS BISNIS ==
svc-katalog : ui katalog, logika katalog, data katalog
svc-pesanan : ui pesanan, logika pesanan, data pesanan
svc-pengiriman : ui pengiriman, logika pengiriman, data pengiriman
fitur "voucher" dikerjakan oleh 1 service: [svc-pesanan]
alur "lihat produk" melewati 1 service: [svc-katalog]Mari kita bedah cara program mengukur kedua potongan:
record Layanan(nama, isi): satu service direpresentasikan sebagai daftar tanggung jawab. Tanggung jawab ditulis dalam polalapisan domain— misalnya"logika pesanan"— sehingga bisa dicari dengancontains(...).penampung(daftar, kebutuhan): untuk satu daftar kebutuhan fitur (misalnya tiga bagian dari fitur voucher), method ini mengumpulkan nama service yang menampung minimal satu kebutuhan, dalamTreeSetagar tercetak terurut.FITUR_VOUCHER: fitur voucher hidup di domain pesanan dan terdiri dari tiga bagian — UI, logika, data. Pada potongan per lapisan, ketiganya tersebar disvc-ui,svc-logika,svc-data; pada potongan per kapabilitas, ketiganya jatuh disvc-pesanan.ALUR_LIHAT_PRODUK: alur baca yang paling sering dipakai pengguna. Potongan per lapisan memaksa satu permintaan “lihat produk” menyeberang tiga service berantai; potongan per kapabilitas menyelesaikannya di dalam satu proses.
Angka pada keluaran adalah coupling yang nyata: potongan lapisan membuat setiap fitur menjadi rilis yang menyentuh tiga tim sekaligus — koordinasi, tes integrasi, dan jadwal yang harus disinkronkan. Potongan kapabilitas membiarkan tim pesanan menyelesaikan voucher sendiri dari ujung ke ujung.
Kapabilitas Bisnis sebagai Pisau #
Kapabilitas bisnis adalah hal yang dihasilkan organisasi bagi pelanggannya — “menyajikan katalog”, “memproses pesanan”, “mengirim paket”. Microservices.io merangkum dua pedoman pemotongan yang paling teruji: decompose by business capability (potong menurut apa yang bisnis lakukan) dan decompose by subdomain (potong menurut peta domain pada Domain-driven Design). Keduanya bersepakat pada satu prinsip: batas service harus mengikuti batas pengetahuan, bukan batas tumpukan teknis.
Kita sudah melatih cara menemukan batas pengetahuan itu pada chapter Domain-driven Design di Section Software Design dan Architecture: setiap bounded context memagari satu bahasa dan satu model. Aturan pratiknya sederhana — bila sebuah istilah punya makna berbeda di dua tempat (Produk untuk katalog berarti halaman pemasaran, untuk gudang berarti unit fisik dengan nomor rak), kedua tempat itu adalah dua service yang berbeda.
Database per Service #
Konsekuensi langsung dari batas kapabilitas: data ikut terbagi. Pola database per service memberi tiap service wadah simpannya sendiri — svc-katalog memiliki tabel produk, svc-pesanan memiliki tabel pesanan — dan tidak ada service lain yang boleh membaca tabel tetangganya langsung; semua akses lintas batas lewat API pemiliknya.
| Aspek | Satu database bersama | Database per service |
|---|---|---|
| Konsistensi | satu transaksi ACID | konsistensi eventual, saga |
| Keterhubungan skema | perubahan tabel menjalar ke semua service | perubahan terkurung milik pemilik |
| Skalabilitas data | satu titik sibuk | per service menskalakan sendiri |
| Akses lintas domain | query JOIN bebas | lewat API atau event |
Baris pertama tabel itu adalah harga yang harus dibayar — dan menjadi alasan chapter Transaksi Terdistribusi serta Saga Pattern ada di section ini.
Ukuran Service yang Sehat #
Ukuran yang tepat bukan sekecil mungkin — service berisi satu tabel atau satu endpoint disebut nanoservice dan menambah beban operasional tanpa manfaat batas. Patokan yang lebih berguna dari jumlah baris: satu service bisa dideploy satu tim dalam satu sore, tugasnya bisa dijelaskan satu kalimat, dan datanya benar-benar miliknya. Bila tiga tanda itu terpenuhi tanpa koordinasi lintas tim, batasnya sehat.
Batas yang baik memotong data, logika, dan tanggung jawab sekaligus — dan begitu batas terpasang, pertanyaan teknis berikutnya segera muncul: bagaimana service-service ini saling berbicara? Dua gaya utamanya, REST sinkron dan messaging asinkron, menjadi bahasan chapter berikutnya.
Referensi #
- microservices.io: Decompose by Business Capability
- microservices.io: Decompose by Subdomain
- microservices.io: Database per Service
- Martin Fowler: Microservices
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
BatasKapabilitas.java
Unduh berkasimport java.util.List;
import java.util.Set;
import java.util.TreeSet;
/** Membandingkan dua cara memotong sistem yang sama menjadi service. */
public class BatasKapabilitas {
/** Satu service beserta daftar tanggung jawab yang ditampungnya. */
record Layanan(String nama, List<String> isi) {}
// Alur "lihat produk": tiga kebutuhan pada domain katalog
static final List<String> ALUR_LIHAT_PRODUK =
List.of("ui katalog", "logika katalog", "data katalog");
// Fitur baru "voucher": tiga kebutuhan pada domain pesanan
static final List<String> FITUR_VOUCHER =
List.of("ui pesanan", "logika pesanan", "data pesanan");
public static void main(String[] args) {
// Potongan A: batas mengikuti lapisan teknis (ui / logika / data)
List<Layanan> perLapisan = List.of(
new Layanan("svc-ui", List.of(
"ui katalog", "ui pesanan", "ui pengiriman")),
new Layanan("svc-logika", List.of(
"logika katalog", "logika pesanan", "logika pengiriman")),
new Layanan("svc-data", List.of(
"data katalog", "data pesanan", "data pengiriman")));
// Potongan B: batas mengikuti kapabilitas bisnis (katalog / pesanan / pengiriman)
List<Layanan> perKapabilitas = List.of(
new Layanan("svc-katalog", List.of(
"ui katalog", "logika katalog", "data katalog")),
new Layanan("svc-pesanan", List.of(
"ui pesanan", "logika pesanan", "data pesanan")),
new Layanan("svc-pengiriman", List.of(
"ui pengiriman", "logika pengiriman", "data pengiriman")));
System.out.println("== POTONGAN PER LAPISAN TEKNIS ==");
tampilkan(perLapisan);
laporan(perLapisan);
System.out.println();
System.out.println("== POTONGAN PER KAPABILITAS BISNIS ==");
tampilkan(perKapabilitas);
laporan(perKapabilitas);
}
static void tampilkan(List<Layanan> daftar) {
for (Layanan l : daftar) {
System.out.println(" " + String.format("%-16s", l.nama()) + ": "
+ String.join(", ", l.isi()));
}
}
static void laporan(List<Layanan> daftar) {
Set<String> voucher = penampung(daftar, FITUR_VOUCHER);
Set<String> lihatProduk = penampung(daftar, ALUR_LIHAT_PRODUK);
System.out.println(" fitur \"voucher\" dikerjakan oleh "
+ voucher.size() + " service: " + voucher);
System.out.println(" alur \"lihat produk\" melewati "
+ lihatProduk.size() + " service: " + lihatProduk);
}
/** Kumpulan service yang menampung minimal satu kebutuhan pada daftar. */
static Set<String> penampung(List<Layanan> daftar, List<String> kebutuhan) {
Set<String> hasil = new TreeSet<>();
for (Layanan l : daftar) {
for (String k : kebutuhan) {
if (l.isi().contains(k)) {
hasil.add(l.nama());
}
}
}
return hasil;
}
}