M
Mr Sugiarto
Developer
03 Oct 2026 7 min read

Bab lalu kita buktikan Gateway Service benar-benar meneruskan request ke Inventory Service. Tapi ada satu sisi yang belum kita bahas: bagaimana Order Service sendiri — bukan client eksternal lewat gateway — memanggil Inventory Service secara langsung, sebagai bagian dari logikanya sendiri. Ini bab terakhir dari seri 29 bab ini, dan sekaligus yang paling konkret menyatukan semua yang sudah kita pelajari: konfigurasi (Fase 1), pemodelan data (Fase 0 & 2), dan yang paling penting — prinsip dari bab 25 bahwa kegagalan satu service tidak boleh menjalar begitu saja ke service lain.

Properti Konfigurasi: InventoryServiceProperties

Sama seperti AppInfoProperties di bab 8, alamat Inventory Service tidak boleh di-hardcode di dalam kode. Order Service punya InventoryServiceProperties.kt:

package com.indokoding.orderservice

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "inventory-service")
data class InventoryServiceProperties(val baseUrl: String)

Dibanding @Value("${services.order-service.uri}") yang dipakai Gateway Service di bab lalu untuk satu nilai tunggal, di sini kita pakai @ConfigurationProperties — cocok karena polanya bisa berkembang (misalnya nanti menambah field seperti timeout atau API key) tanpa menambah parameter @Value satu per satu. Nilainya diisi dari application.yml:

inventory-service:
  base-url: http://localhost:8081

dan di-override di application-prod.yml lewat ${INVENTORY_SERVICE_URL} — pola environment-variable yang sama seperti datasource production yang kita bahas di bab 18.

Timeout Bukan Opsional: InventoryClientConfig

package com.indokoding.orderservice

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.client.SimpleClientHttpRequestFactory
import org.springframework.web.client.RestClient
import java.time.Duration

@Configuration
class InventoryClientConfig {

    // Timeout wajib diset eksplisit - tanpa ini, satu Inventory Service yang lambat
    // atau macet bisa membuat thread di Order Service ikut menunggu tanpa batas waktu,
    // kegagalan menjalar dari satu service ke service lain (lihat Fase 4.1).
    @Bean
    fun inventoryRestClient(properties: InventoryServiceProperties): RestClient {
        val requestFactory = SimpleClientHttpRequestFactory().apply {
            setConnectTimeout(Duration.ofSeconds(2))
            setReadTimeout(Duration.ofSeconds(3))
        }
        return RestClient.builder()
            .baseUrl(properties.baseUrl)
            .requestFactory(requestFactory)
            .build()
    }
}

Ingat kembali teori bab 25: dalam microservice, kegagalan satu service bisa menjalar (cascading failure) ke service lain kalau tidak dijaga. Ini bukan skenario abstrak — kalau RestClient di sini dibiarkan tanpa batas waktu, dan suatu hari Inventory Service macet (bukan down total, tapi lambat merespons — sering justru lebih berbahaya dari down total), setiap thread Order Service yang menangani request inventory-check akan menunggu tanpa batas menunggu balasan yang tidak pernah datang. Kalau ini terjadi berulang, thread pool Order Service bisa habis, dan Order Service pun ikut lumpuh — padahal masalah aslinya cuma di Inventory Service. Dua baris setConnectTimeout/setReadTimeout ini adalah pertahanan paling dasar terhadap skenario itu: 2 detik untuk membuka koneksi, 3 detik untuk menunggu respons, setelah itu panggilan dianggap gagal dan dilempar sebagai exception.

InventoryClient — Panggilan Sinkron dengan Graceful Degradation

package com.indokoding.orderservice

import org.springframework.stereotype.Component
import org.springframework.web.client.RestClient
import org.springframework.web.client.RestClientException
import org.springframework.web.client.body

data class StockResponse(val itemName: String, val stock: Int)

@Component
class InventoryClient(private val inventoryRestClient: RestClient) {

    fun getStock(itemName: String): Int? =
        try {
            inventoryRestClient.get()
                .uri("/api/inventory/{itemName}", itemName)
                .retrieve()
                .body<StockResponse>()
                ?.stock
        } catch (ex: RestClientException) {
            null
        }
}

Perhatikan .uri("/api/inventory/{itemName}", itemName) — pola templating URI yang sama seperti @PathVariable di controller (bab 9), cuma di sisi client: RestClient yang menangani encoding karakter spesial (misalnya spasi di "Keyboard Mechanical" jadi %20) secara otomatis, kita tidak perlu membangun string URL manual. .body<StockResponse>() adalah extension function Kotlin idiomatis dari Spring untuk deserialisasi response JSON langsung ke tipe yang diminta lewat generic reified — ekspresinya lebih ringkas dibanding versi Java yang butuh .body(StockResponse::class.java) eksplisit.

Bagian paling penting justru ada di catch (ex: RestClientException). getStock mengembalikan Int? (nullable) — bukan Int — dan sengaja menangkap semua RestClientException lalu mengembalikan null, apa pun penyebabnya: timeout habis, koneksi ditolak (Inventory Service down), atau bahkan item benar-benar tidak ditemukan (404, yang oleh RestClient.retrieve() juga dilempar sebagai exception turunan RestClientException). Ini simplifikasi yang disengaja — secara teknis, "service tidak bisa dihubungi" dan "item tidak ditemukan" adalah dua kondisi yang berbeda, dan bisa dibedakan dengan menambahkan .onStatus(...) untuk menangani status code tertentu secara spesifik. Untuk cakupan bab ini, kita anggap sama: dari sudut pandang Order Service, keduanya berarti "saya tidak bisa memberi tahu kamu angka stok yang pasti sekarang" — penanganan kegagalan yang lebih presisi (circuit breaker, retry dengan backoff, dan pembedaan jenis error) adalah materi Fase 6 (Resilience & Observability) yang belum ditulis di roadmap.

Endpoint GET /inventory-check/{itemName}

Di OrderController.kt:

@GetMapping("/inventory-check/{itemName}")
fun checkInventory(@PathVariable itemName: String): Map<String, Any?> {
    val stock = inventoryClient.getStock(itemName)
    return if (stock != null) {
        mapOf("itemName" to itemName, "stock" to stock, "available" to (stock > 0))
    } else {
        mapOf(
            "itemName" to itemName,
            "stock" to null,
            "available" to null,
            "note" to "Inventory Service tidak bisa dihubungi atau item tidak ditemukan"
        )
    }
}

Perhatikan baik-baik: endpoint ini selalu mengembalikan HTTP 200, baik saat Inventory Service merespons normal maupun saat panggilan gagal total. Kalau stock bernilai null (karena InventoryClient.getStock menangkap exception-nya), responsnya tetap 200 dengan stock: null dan sebuah note yang menjelaskan kenapa. Ini yang disebut graceful degradation: Order Service tidak ikut gagal (500) hanya karena satu service lain sedang bermasalah — ia tetap merespons ke client, dengan informasi yang lebih terbatas, tapi tidak berhenti total.

Membuktikannya Lewat Test

Dua test di OrderControllerTest.kt (dari bab 23) memverifikasi persis perilaku ini, dengan InventoryClient di-mock lewat @MockkBean — tidak butuh Inventory Service sungguhan berjalan untuk memverifikasi logika di endpoint ini:

@Test
fun `GET inventory-check mengembalikan stok saat Inventory Service bisa dihubungi`() {
    every { inventoryClient.getStock("Keyboard Mechanical") } returns 12

    mockMvc.get("/api/orders/inventory-check/{itemName}", "Keyboard Mechanical").andExpect {
        status { isOk() }
        jsonPath("$.stock") { value(12) }
        jsonPath("$.available") { value(true) }
    }
}

@Test
fun `GET inventory-check tetap mengembalikan 200 saat Inventory Service tidak bisa dihubungi`() {
    every { inventoryClient.getStock("Webcam") } returns null

    mockMvc.get("/api/orders/inventory-check/{itemName}", "Webcam").andExpect {
        status { isOk() }
        jsonPath("$.stock") { value(null) }
    }
}

Test kedua ini yang paling penting untuk dibaca dua kali: dengan inventoryClient.getStock(...) dipaksa returns null (mensimulasikan Inventory Service gagal dihubungi, tanpa perlu benar-benar mematikan service apa pun), assertion-nya tetap status { isOk() } — bukan isInternalServerError() atau semacamnya. Ini bukti langsung, bukan janji di komentar kode, bahwa graceful degradation di checkInventory benar-benar berfungsi seperti yang diklaim.

Menutup Seri: Apa yang Sudah Kita Bangun

Sampai di sini, seri 29 bab ini selesai. Mari rekap perjalanannya:

  • Fase 0 (bab 1-6) membekali fundamental bahasa Kotlin — null safety, OOP, functional programming, collections, coroutines, error handling — semuanya lewat test asli, bukan cuplikan kode yang berdiri sendiri.
  • Fase 1-3 (bab 7-24) membangun satu service yang solid: Order Service, lengkap dengan REST API, database PostgreSQL asli lewat Flyway dan Spring Data JPA, dan yang paling ditekankan di seri ini — rangkaian test berlapis (unit, parameterized, integration, Testcontainers, controller test) yang semuanya hijau dan dijalankan otomatis lewat CI.
  • Fase 4 (bab 25-29) mengubah satu service itu jadi sistem dua-service sungguhan: Inventory Service berdiri sendiri, Gateway Service jadi pintu masuk tunggal, dan Order Service memanggil Inventory Service lewat HTTP dengan timeout dan penanganan kegagalan yang benar — bukan asumsi bahwa service lain akan selalu hidup dan cepat.

Yang membedakan seri ini dari sekadar kumpulan cuplikan kode: tiga project di balik seri ini (order-service/, inventory-service/, gateway-service/) beneran bisa di-clone dan dijalankan, seluruh test suite-nya hijau, dan routing Gateway sudah dibuktikan lewat curl sungguhan di bab 28 — bukan kode yang "kelihatannya benar" tapi tidak pernah dijalankan.

Roadmap belajar yang lebih besar masih menyisakan lima fase: Fase 5 (event-driven messaging dengan Kafka), Fase 6 (resilience dengan circuit breaker, dan observability lewat tracing/metrics), Fase 7 (security dengan Spring Security + JWT), Fase 8 (containerization dan deployment), dan Fase 9 (capstone project yang merangkai semuanya). Itu perjalanan lanjutan untuk seri berikutnya.

Selamat — kalau kamu sampai di titik ini sambil benar-benar mengikuti kode dan menjalankan test-nya sendiri, kamu sudah membangun sistem microservice Kotlin yang nyata, teruji, dan bisa dipercaya. Itu bukan pencapaian kecil.

M
Mr Sugiarto

Developer

Bagian dari Series: Kotlin Backend Microservice - Belajar Fundamental sampai Microservice Nyata

Belajar Kotlin + Spring Boot dari fundamental bahasa (null safety, OOP, coroutines) sampai membangun sistem microservice sungguhan - REST API, Postgre...

Lihat Series Lengkap
Artikel Terkait