Empat bab lalu kita fokus pada kode yang berjalan sinkron — setiap baris selesai sebelum baris berikutnya dimulai. Tapi backend nyata jarang sesederhana itu: Order Service yang akan kita bangun nanti perlu memanggil Inventory Service lewat HTTP, query database, atau kirim event — semuanya operasi I/O yang butuh waktu tunggu (network latency, disk, dsb). Bab ini membahas bagaimana Kotlin menangani itu lewat coroutines, lewat test asli CoroutinesBasicsTest.kt.
Masalah yang Coroutines Selesaikan
Kalau sebuah thread menunggu response dari service lain secara blocking (thread berhenti total sampai response datang), thread itu tidak bisa mengerjakan apapun selama menunggu — padahal thread adalah resource yang mahal dan jumlahnya terbatas. Di aplikasi dengan banyak request bersamaan, pola blocking ini gampang bikin server kehabisan thread meski sebenarnya CPU-nya menganggur, cuma menunggu network.
Solusi klasik adalah callback atau reactive stream — kode ditulis dalam bentuk "kalau response datang, jalankan fungsi ini" — tapi begitu ada beberapa langkah berurutan, kode jadi bertumpuk callback dalam callback (istilahnya callback hell), atau rangkaian operator reactive yang susah ditelusuri urutan eksekusinya. Coroutines Kotlin menawarkan jalan tengah: kode non-blocking, tapi ditulis seperti kode sequential biasa — tanpa callback bertumpuk, tanpa operator chain reactive yang rumit.
suspend fun: Fungsi yang Bisa "Dijeda"
Fondasinya adalah kata kunci suspend pada deklarasi fungsi. Test file ini mensimulasikan pemanggilan service lain lewat network — persis skenario yang akan kita hadapi sungguhan saat Order Service memanggil Inventory Service di Fase 4:
// suspend fun: mensimulasikan pemanggilan service lain lewat network - non-blocking.
private suspend fun checkStock(itemName: String): Int {
delay(10) // simulasi latency network
return when (itemName) {
"Keyboard" -> 12
"Mouse" -> 0
else -> -1
}
}
delay(10) di sini adalah versi suspend dari "tunggu 10 milidetik" — beda dari Thread.sleep(10) yang memblokir thread sepenuhnya, delay melepas thread supaya bisa dipakai mengerjakan pekerjaan lain selama menunggu, lalu coroutine ini dilanjutkan lagi begitu waktunya selesai. Kata kunci suspend di depan fun menandai bahwa fungsi ini punya kemampuan "dijeda" seperti itu — dan sebagai konsekuensinya, suspend fun cuma bisa dipanggil dari coroutine lain atau dari fungsi suspend lain, tidak dari fungsi biasa sembarangan. Ini bukan sekadar konvensi penamaan; compiler yang menegakkannya.
Memanggil suspend fun: Tetap Terbaca Sequential
Test pertama menunjukkan bagaimana memanggil suspend fun terlihat sama persis seperti memanggil fungsi biasa — tidak ada callback, tidak ada .then():
@Test
fun `suspend function berjalan sequential tanpa callback`() = runTest {
val stock = checkStock("Keyboard")
assertEquals(12, stock)
}
val stock = checkStock("Keyboard") terbaca sama seperti memanggil fungsi sinkron biasa, padahal di baliknya thread yang menjalankan test ini tidak diblokir selama delay(10) berjalan. Inilah yang dimaksud "ditulis seperti kode sequential" — kompleksitas non-blocking-nya ditangani coroutine machinery, bukan ditulis eksplisit oleh kita sebagai callback bersarang.
Perhatikan juga runTest { } yang membungkus body test. Ini fungsi dari library kotlinx-coroutines-test, dibutuhkan karena suspend fun seperti checkStock hanya bisa dipanggil dari dalam sebuah coroutine scope — runTest menyediakan scope itu khusus untuk kebutuhan testing, sekaligus mempercepat delay() secara virtual supaya test tidak benar-benar menunggu waktu asli (test ini selesai dalam hitungan milidetik meski isinya delay(10), bukan karena angkanya kecil, tapi karena runTest men-skip waktu tunggu virtual itu).
async/awaitAll: Menjalankan Beberapa Pemanggilan Secara Konkuren
Test kedua adalah bagian paling penting untuk konteks backend: menjalankan dua pemanggilan suspend fun secara bersamaan, bukan satu-satu berurutan.
@Test
fun `async-await menjalankan beberapa suspend function secara konkuren`() = runTest {
// Dua panggilan checkStock berjalan konkuren (structured concurrency),
// bukan menunggu satu selesai baru mulai yang lain.
val keyboardStock = async { checkStock("Keyboard") }
val mouseStock = async { checkStock("Mouse") }
val results = awaitAll(keyboardStock, mouseStock)
assertEquals(listOf(12, 0), results)
}
async { } memulai sebuah coroutine baru dan langsung mengembalikan kontrol ke baris berikutnya tanpa menunggu hasilnya — mengembalikan objek Deferred yang menyimpan "janji" hasil di masa depan. Karena keyboardStock dan mouseStock sama-sama dimulai lewat async sebelum ada yang ditunggu, keduanya berjalan konkuren: Kotlin tidak menunggu checkStock("Keyboard") selesai dulu baru memulai checkStock("Mouse"). Baru di baris awaitAll(keyboardStock, mouseStock) kita benar-benar menunggu — dan menunggunya sekaligus untuk kedua hasil, bukan berurutan.
Bandingkan dengan menulis dua checkStock berurutan tanpa async (val a = checkStock("Keyboard"); val b = checkStock("Mouse")) — itu tetap berjalan, tapi total waktu tunggunya adalah jumlah kedua delay tersebut, karena panggilan kedua baru mulai setelah yang pertama benar-benar selesai. Dengan async/awaitAll, total waktu tunggunya kira-kira sama dengan delay yang paling lama di antara keduanya, karena keduanya berjalan bersamaan. Untuk dua pemanggilan network, bedanya bisa signifikan — dan ini akan jadi pola yang benar-benar kita pakai di Fase 4, saat Order Service perlu mengecek stok ke Inventory Service dan mengecek data pelanggan ke service lain sekaligus, tanpa harus menunggu satu per satu.
Kenapa Ini Penting untuk Backend
Di bahasa tanpa dukungan coroutine, menjalankan dua panggilan I/O secara konkuren biasanya berarti mengelola thread pool manual, atau menulis rangkaian callback/Future.thenCombine yang jauh lebih sulit dibaca dibanding dua baris async di atas. suspend/async/awaitAll di Kotlin membuat kode non-blocking tetap terbaca linear dari atas ke bawah — sesuatu yang jadi krusial begitu Order Service kita benar-benar mulai bicara ke service lain lewat jaringan.
Sejauh ini kita fokus pada jalur "semua berjalan lancar." Bab terakhir Fase 0 membahas sisi sebaliknya: bagaimana Kotlin merepresentasikan kegagalan secara idiomatis, lewat Result dan sealed class, sebelum kita benar-benar mulai membangun Order Service di Fase 1.
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