Tampilan
Kenapa penting
Begitu monolith dipecah per bounded context, panggilan method yang dulu memakan nanodetik berubah menjadi panggilan jaringan yang bisa lambat, gagal, atau mengembalikan data dengan bentuk yang tidak kamu duga. Sebagian besar insiden di sistem microservices bukan karena logika bisnis salah, melainkan karena komunikasi antar-service: timeout yang lupa dipasang, perubahan field yang mematahkan consumer, atau satu halaman yang memicu puluhan panggilan internal. Bab ini membahas cara merancang komunikasi sinkron (request–response) yang tangguh dan kapan sebaiknya kamu tidak memakainya sama sekali.
Konsep inti
REST antar-service dengan RestClient
Di M11 kamu sudah memakai RestClient dan HTTP interface client. Di dunia microservices, polanya sama, tetapi ada tiga aturan tambahan: selalu pasang timeout, bungkus dengan resilience (retry terbatas + circuit breaker), dan perlakukan response sebagai data asing — jangan memakai entity milik service lain, buat DTO sendiri.
java
// Potongan — Spring Boot 4.1 (Spring Framework 7)
@Configuration
class InventoryClientConfig {
@Bean
InventoryClient inventoryClient(RestClient.Builder builder) {
var factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofMillis(300));
factory.setReadTimeout(Duration.ofSeconds(1));
RestClient restClient = builder
.baseUrl("http://inventory-service")
.requestFactory(factory)
.build();
return HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build()
.createClient(InventoryClient.class);
}
}
interface InventoryClient {
@GetExchange("/stocks/{sku}")
StockResponse getStock(@PathVariable String sku);
}
// DTO milik order-service — bukan class milik inventory-service
record StockResponse(String sku, int available) {}Retry hanya aman untuk operasi idempotent (GET, atau POST yang membawa idempotency key seperti di M11). Retry pada POST /payments tanpa kunci idempotensi bisa menagih pelanggan dua kali.
Kontrak API dan kompatibilitas mundur
Kontrak API adalah janji kepada consumer: URL, method, field, tipe, kode status, dan arti error. Karena tiap service di-deploy mandiri, provider harus bisa rilis versi baru tanpa menunggu semua consumer ikut berubah.
| Perubahan | Kompatibel mundur? |
|---|---|
| Menambah field baru di response | Ya |
| Menambah field opsional di request | Ya |
| Menambah endpoint baru | Ya |
| Menghapus atau mengganti nama field | Tidak |
Mengubah tipe field (int → string) | Tidak |
| Menjadikan field opsional wajib | Tidak |
| Mengubah arti nilai enum yang ada | Tidak |
Dua kebiasaan menjaga kompatibilitas. Di sisi consumer, terapkan tolerant reader: abaikan field yang tidak dikenal (Jackson di Spring Boot sudah begitu secara default) dan hanya baca field yang benar-benar dipakai. Di sisi provider, pakai pola expand–contract: tambahkan yang baru, biarkan yang lama hidup berdampingan, migrasikan consumer, baru hapus yang lama.
Versioning API
Bila perubahan yang mematahkan tidak terhindarkan, buat versi baru. Pilihan umum:
| Strategi | Contoh | Catatan |
|---|---|---|
| Path | /v2/orders/{id} | Paling jelas, mudah di-route di gateway |
| Header | API-Version: 2 | URL tetap bersih, kurang terlihat |
| Media type | Accept: application/vnd.toko.order.v2+json | Paling "REST", paling repot |
| Query parameter | /orders/{id}?version=2 | Sederhana, mudah tercampur cache |
Spring Framework 7 menambahkan dukungan versioning bawaan: atribut version pada @RequestMapping dan turunannya, dengan sumber versi (header, path, query, atau media type) yang dikonfigurasi terpusat.
java
// Potongan — Spring Framework 7; sumber versi dikonfigurasi, mis. lewat header
@RestController
@RequestMapping("/orders")
class OrderController {
@GetMapping(path = "/{id}", version = "1")
OrderV1 getV1(@PathVariable long id) { /* ... */ }
@GetMapping(path = "/{id}", version = "2")
OrderV2 getV2(@PathVariable long id) { /* ... */ }
}Versi baru adalah beban: dua versi harus dirawat dan dites. Utamakan perubahan aditif; buat versi baru hanya bila benar-benar perlu, dan umumkan tanggal penghentian versi lama.
Latency berantai dan timeout budget
Panggilan sinkron membentuk rantai: pengguna → order → inventory → pricing → payment. Latency serial dijumlahkan, dan availability dikalikan. Program berikut menghitung keduanya untuk rantai checkout:
java
import java.util.List;
public class LatencyBudget {
record Hop(String service, long p99Millis, double availability) {}
public static void main(String[] args) {
List<Hop> chain = List.of(
new Hop("order-service", 40, 0.999),
new Hop("inventory-service", 120, 0.999),
new Hop("pricing-service", 80, 0.999),
new Hop("payment-service", 300, 0.995));
long sequential = 0;
long parallelMax = 0;
double availability = 1.0;
for (Hop hop : chain) {
sequential += hop.p99Millis();
parallelMax = Math.max(parallelMax, hop.p99Millis());
availability *= hop.availability();
}
System.out.println("Total berantai (serial) : " + sequential + " ms");
System.out.println("Paralel (terlama) : " + parallelMax + " ms");
System.out.printf("Availability gabungan : %.2f%%%n", availability * 100);
long budget = 500;
long remaining = budget;
for (Hop hop : chain) {
remaining -= hop.p99Millis();
System.out.println("Sisa budget setelah " + hop.service() + ": " + remaining + " ms");
}
}
}text
Total berantai (serial) : 540 ms
Paralel (terlama) : 300 ms
Availability gabungan : 99.20%
Sisa budget setelah order-service: 460 ms
Sisa budget setelah inventory-service: 340 ms
Sisa budget setelah pricing-service: 260 ms
Sisa budget setelah payment-service: -40 msEmpat service yang masing-masing "99,9%" menghasilkan rantai yang hanya 99,2% — sekitar 5,8 jam gangguan per bulan. Budget 500 ms juga sudah jebol di hop terakhir. Pelajarannya: timeout di hilir harus lebih kecil dari timeout di hulu. Kalau gateway menyerah setelah 2 detik tetapi order-service menunggu payment 5 detik, order-service terus bekerja untuk request yang sudah ditinggalkan pengguna. Pola lanjutannya (deadline propagation, bulkhead lintas service) dibahas di M14.
Chatty API, agregasi, dan BFF
API disebut chatty bila satu kebutuhan bisnis memerlukan banyak panggilan kecil — mis. ambil pesanan, lalu loop setiap item untuk memanggil GET /products/{id}. Ini adalah masalah N+1 (M08) yang sekarang terjadi di jaringan.
Perbaikannya:
- Endpoint batch —
GET /products?ids=1,2,3menggantikan N panggilan. - Agregasi di service pemanggil — panggil beberapa service secara paralel, lalu gabungkan.
- BFF (Backend for Frontend) — service tipis khusus satu jenis klien (web, mobile) yang mengagregasi data dari beberapa service dan membentuk response persis untuk layar tersebut. BFF tidak berisi aturan bisnis inti; aturan tetap milik service domain.
Kalau dua service selalu harus saling memanggil untuk menyelesaikan hampir semua operasi, itu tanda batas bounded context-nya keliru — kembali ke bab sebelumnya sebelum menambal dengan agregasi.
Service-to-service auth (pengantar)
Jaringan internal bukan zona tepercaya. Setiap service sebaiknya memverifikasi siapa pemanggilnya. Dua pendekatan umum: token OAuth2 (client credentials untuk identitas service, atau token relay untuk meneruskan identitas pengguna) dan mTLS (kedua sisi saling menunjukkan sertifikat). Detailnya ada di M14; untuk sekarang, cukup ingat bahwa client antar-service harus mengirim kredensial, dan service harus menolak panggilan tanpa kredensial.
Sinkron atau asinkron?
| Pertanyaan | Sinkron (REST/gRPC) | Asinkron (event/queue) |
|---|---|---|
| Pemanggil butuh jawaban sekarang untuk melanjutkan? | Ya | Tidak |
| Boleh terjadi beberapa detik kemudian? | Tidak ideal | Ya |
| Hilir boleh mati sementara tanpa menggagalkan alur? | Tidak | Ya |
| Ada banyak pihak yang perlu bereaksi? | Repot | Cocok (pub/sub) |
Aturan praktis: query (membaca data untuk ditampilkan) dan validasi yang menentukan hasil langsung cocok sinkron; reaksi terhadap fakta yang sudah terjadi ("pesanan dibayar") cocok asinkron. Event-driven dibahas di bab 5.
Contoh di dunia backend
Order-service memvalidasi stok sebelum membuat pesanan. Panggilan dibungkus timeout, circuit breaker Resilience4j (M11), dan error dari hilir diterjemahkan menjadi error domain milik order-service sendiri.
java
// Potongan — Spring Boot 4.1 + resilience4j-spring-boot (2.4.x)
@Service
class OrderPlacementService {
private final InventoryClient inventory;
private final OrderRepository orders;
OrderPlacementService(InventoryClient inventory, OrderRepository orders) {
this.inventory = inventory;
this.orders = orders;
}
@CircuitBreaker(name = "inventory", fallbackMethod = "inventoryUnavailable")
public OrderResult place(PlaceOrderCommand cmd) {
StockResponse stock = inventory.getStock(cmd.sku());
if (stock.available() < cmd.quantity()) {
return new OrderResult.Rejected("Stok tidak cukup untuk " + cmd.sku());
}
Order saved = orders.save(Order.pending(cmd));
return new OrderResult.Accepted(saved.id());
}
// Dipanggil saat circuit terbuka atau panggilan gagal/timeout
private OrderResult inventoryUnavailable(PlaceOrderCommand cmd, Throwable cause) {
return new OrderResult.Rejected("Layanan stok sedang sibuk, coba lagi sebentar lagi");
}
}
sealed interface OrderResult {
record Accepted(long orderId) implements OrderResult {}
record Rejected(String reason) implements OrderResult {}
}yaml
# application.yml order-service
resilience4j:
circuitbreaker:
instances:
inventory:
sliding-window-size: 20
failure-rate-threshold: 50
wait-duration-in-open-state: 10sPerhatikan bahwa fallback tidak berpura-pura stok tersedia. Fallback yang mengarang data sukses lebih berbahaya daripada menolak dengan pesan jelas.
Pertanyaan interview
1. Apa yang dimaksud kompatibilitas mundur pada API, dan bagaimana kamu menjaganya?
Versi baru provider tetap bisa dipakai consumer lama tanpa perubahan. Caranya: hanya melakukan perubahan aditif (field atau endpoint baru), consumer menerapkan tolerant reader, dan perubahan yang mematahkan dilakukan lewat expand–contract atau versi baru dengan masa transisi.
2. Kenapa timeout di service hilir sebaiknya lebih pendek dari service di hulu?
Agar service hilir menyerah lebih dulu dan hulu masih punya waktu menangani kegagalan (fallback atau pesan error). Bila sebaliknya, hulu sudah menyerah sementara hilir terus memakan thread dan koneksi untuk pekerjaan yang hasilnya tidak akan dipakai.
3. Apa itu BFF dan kapan kamu memakainya?
Backend for Frontend adalah service khusus satu jenis klien yang mengagregasi beberapa service domain menjadi response sesuai kebutuhan layar. Dipakai saat klien (terutama mobile) harus memanggil banyak endpoint untuk satu layar, atau saat kebutuhan web dan mobile berbeda jauh. BFF tidak boleh menjadi tempat aturan bisnis inti.
Jebakan umum
- Tidak memasang timeout. Satu service lambat menyeret semua pemanggilnya. Selalu set connect dan read timeout.
- Retry tanpa batas atau pada operasi non-idempotent. Retry memperparah beban saat hilir sedang kesulitan dan bisa menggandakan transaksi. Batasi jumlahnya, pakai backoff, dan hanya untuk operasi idempotent.
- Memakai class milik service lain sebagai DTO (mis. lewat library bersama). Perubahan internal provider memaksa semua consumer rebuild — kopling yang ingin dihindari microservices.
- Mengganti nama atau menghapus field tanpa masa transisi. Consumer yang belum deploy langsung patah.
- Rantai sinkron yang panjang (A → B → C → D → E). Latency dijumlahkan dan availability dikalikan; pertimbangkan paralelisasi, cache, atau event.
- Fallback yang mengarang data sukses, mis. menganggap stok selalu ada saat inventory mati.
Ringkasan
- Panggilan antar-service adalah panggilan jaringan: pasang timeout, retry terbatas untuk operasi idempotent, dan circuit breaker.
- Jaga kompatibilitas mundur dengan perubahan aditif, tolerant reader, dan expand–contract; versi baru hanya bila terpaksa.
- Latency serial dijumlahkan dan availability dikalikan; timeout hilir harus lebih kecil dari hulu.
- Chatty API diperbaiki dengan endpoint batch, agregasi paralel, atau BFF — atau dengan meninjau ulang batas service.
- Pakai sinkron untuk query dan validasi yang butuh jawaban segera; asinkron untuk reaksi terhadap fakta.