Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.
01Apa beda komunikasi synchronous dan asynchronous pada microservices?
Pada komunikasi synchronous, satu service memanggil API service lain lewat HTTP atau gRPC dan menunggu response. Pada asynchronous, service mengirim message lalu lanjut, dan service lain memprosesnya belakangan lewat broker. Yang pertama mengikat kedua service dalam waktu, yang kedua membiarkan penerima mati saat message dikirim.
02Kapan sebaiknya memakai asynchronous messaging dibanding REST antar service?
Pakai messaging saat caller tidak butuh hasilnya untuk melanjutkan: side effect seperti struk dan notifikasi, fan-out ke beberapa consumer, job yang lama, dan lonjakan yang bisa diratakan queue. Tetap pakai REST atau gRPC untuk query yang ditunggu user. Putuskan per interaksi, bukan sekali untuk seluruh sistem.
03Seberapa besar synchronous call berantai menurunkan availability?
Jika setiap service dalam chain wajib, availability adalah hasil kali angka masing-masing. Lima service di 99,9 persen menghasilkan 0,999 pangkat lima, sekitar 99,501 persen, atau kira-kira 215,6 menit downtime dalam bulan 30 hari. Hitungan ini mengasumsikan failure independen, jadi ia panduan bentuk, bukan ramalan.
04Apakah HTTP selalu synchronous?
HTTP adalah protocol synchronous karena pengirim menunggu response, meskipun client memakai non-blocking I/O. Kamu tetap bisa membangun perilaku asynchronous di atasnya: langsung kembalikan 202 Accepted dan biarkan client polling status endpoint atau menerima webhook saat pekerjaan selesai.
05Apa arti 202 Accepted dan kapan sebaiknya dikembalikan?
RFC 9110 mendefinisikan 202 sebagai diterima untuk diproses tetapi belum selesai, dan menyebut response-nya sengaja noncommittal. Kembalikan untuk pekerjaan yang lebih lama dari seharusnya sebuah request, seperti laporan atau export, beserta status URL untuk polling. Karena request masih bisa gagal nanti, status endpoint harus bisa melaporkan kegagalan.
Komunikasi Sync vs Async Microservices: Cara Memilih
Kapan service sebaiknya memanggil service lain lalu menunggu, dan kapan cukup publish message: hitungan availability, latency, penanganan failure, consistency, dan hybrid 202 Accepted.
Pakai synchronous call saat caller butuh jawabannya untuk melanjutkan, dan asynchronous messaging untuk side effect, fan-out, dan pekerjaan lama. Synchronous call berantai mengalikan availability: lima service masing-masing 99,9 persen menghasilkan sekitar 99,5 persen. Pilih per interaksi, lalu gabungkan keduanya dengan response 202 Accepted dan status endpoint.
Pertanyaan ini muncul begitu service kedua hadir. POS carwash selesai memproses pembayaran, lalu ada hal lain yang harus terjadi: struk dikirim, stok disesuaikan, counter laporan bergerak. Apakah kamu memanggil tiap service itu dan menunggu, atau cukup mengumumkan bahwa pembayaran terjadi dan membiarkan service lain bereaksi?
Post ini adalah panduan keputusan untuk pilihan tersebut. Saya tidak menjalankan benchmark apa pun. Definisi dan trade-off berasal dari Azure Architecture Center dan microservices.io, perilaku 202 dari RFC 9110, dan setiap angka di bawah adalah hitungan yang saya turunkan dan tunjukkan lengkap, memakai angka asumsi yang diberi label asumsi.
Apa beda komunikasi synchronous dan asynchronous?
Pada komunikasi synchronous, sebuah service memanggil API yang dibuka service lain lewat HTTP atau gRPC, dan caller menunggu response. Pada asynchronous messaging, service mengirim message tanpa menunggu balasan, dan satu atau lebih service memprosesnya nanti. Azure Architecture Center membuat satu pembedaan yang layak diingat: asynchronous I/O bukan asynchronous protocol. HTTP client bisa memakai non-blocking I/O, tetapi HTTP tetap protocol synchronous karena pengirim menunggu jawaban.
Dimensi
Synchronous request/response
Asynchronous messaging
Apakah caller menunggu?
Ya, sampai response datang
Tidak, lanjut setelah hand-off
Coupling dalam waktu
Kedua service harus hidup di saat yang sama
Consumer boleh mati; broker menyimpan message
Jika callee mati
Caller menerima error atau timeout
Pengirim tetap berhasil; message diproses setelah pulih
Latency yang dilihat user
Jumlah semua hop dalam chain
Hanya hand-off; pekerjaan selesai belakangan
Consistency
Jawaban langsung, mudah dipikirkan
Eventual; consumer harus menoleransi duplikat
Komponen tambahan
Tidak ada selain network
Broker yang juga harus tetap available
Tabel ini adalah seluruh argumennya dalam bentuk ringkas. Synchronous call memberi jawaban langsung yang sederhana dan menukarnya dengan coupling dalam waktu. Messaging memberi independensi dan menukarnya dengan broker, penanganan duplikat, dan cara yang lebih sulit untuk mengembalikan response. Panduan Azure mencatat biaya yang sama: kompleksitas, latency antrean saat queue penuh, dan sulitnya membangun request-response di atas messaging.
Berapa availability yang hilang pada synchronous call berantai?
Jika sebuah request membutuhkan setiap service dalam chain untuk menjawab, availability chain adalah hasil kali availability masing-masing. Microservices.io menyebut kelemahan yang sama dengan kata-kata: pada remote procedure invocation, client dan service harus sama-sama available selama interaksi. Hitungan ini mengasumsikan failure saling independen dan setiap hop wajib, jadi anggap angkanya batas atas untuk bentuk ini, bukan prediksi untuk sistemmu.
Jumlah service dalam chain, masing-masing 99,9 persen
Availability chain
Downtime per 30 hari
1
99,900 persen
43,2 menit
2
99,800 persen
86,4 menit
3
99,700 persen
129,5 menit
5
99,501 persen
215,6 menit
10
99,004 persen
430,1 menit
20
98,019 persen
855,8 menit
Kode berikut menghitung tabelnya. Satu bulan 30 hari punya 43.200 menit, dan downtime adalah bagian yang tidak available dari angka itu.
// availability.ts - run with: node availability.ts (Node 22.18+ strips the types itself)
const MINUTES_PER_30_DAYS = 30 * 24 * 60; // 43,200
function chain(perService: number, n: number) {
// Every hop is mandatory and failures are assumed independent, so availabilities multiply.
const availability = perService ** n;
return {
n,
availabilityPct: (availability * 100).toFixed(3),
downtimeMinutes: ((1 - availability) * MINUTES_PER_30_DAYS).toFixed(1),
};
}
for (const n of [1, 2, 3, 5, 10, 20]) console.log(chain(0.999, n));
// n=1 99.900 43.2
// n=2 99.800 86.4
// n=3 99.700 129.5
// n=5 99.501 215.6
// n=10 99.004 430.1
// n=20 98.019 855.8
Lima service yang masing-masing tampak sehat di 99,9 persen menghasilkan jalur user dengan downtime sekitar lima kali lipat satu service. Messaging mengubah bentuk penjumlahan ini. Request ke user bergantung pada service yang memang harus benar saat itu juga ditambah broker, sedangkan sisanya diisi dari queue dan boleh mati tanpa menggagalkan request. Broker kini menjadi dependency dengan availability sendiri, yang oleh microservices.io disebut kelemahan utama messaging, jadi di satu VPS ia baru membantu kalau dipantau seserius database.
Bagaimana latency menumpuk pada synchronous call berantai?
Latency pada chain berurutan adalah jumlah semua hop, dan dependency paling lambat menentukan batas bawah seluruh request. Panduan Azure menyebut efek yang sama: jika service A memanggil B yang memanggil C, menunggu synchronous call bisa menambah latency yang tidak dapat diterima. Timeout dan retry lalu membuat kasus terburuk jauh lebih buruk daripada kasus tipikal.
Setiap angka pada snippet ini adalah asumsi untuk memperlihatkan hitungannya, bukan hasil pengukuran.
// Assumed: five sequential hops, 40 ms each. Latency adds: 5 * 40 = 200 ms
// before the caller has done any work of its own.
// Assumed: a 2000 ms timeout and 3 attempts per call.
// Worst case at ONE layer = 3 * 2000 = 6000 ms, plus the backoff waits between attempts.
// Assumed: 3 layers, each allowing 3 attempts against the layer below.
// Calls reaching the deepest service for one user action = 3 * 3 * 3 = 27.
// Right: one deadline for the whole request, each hop spends only what is left.
const DEADLINE_MS = 1500;
const startedAt = Date.now();
const remaining = () => Math.max(0, DEADLINE_MS - (Date.now() - startedAt));
// pass remaining() as the timeout of the NEXT call, never a fresh fixed number
Aturan praktisnya: beri request teratas satu deadline, dan biarkan tiap hop hanya memakai sisa waktunya. Kebijakan retry layak didesain tersendiri, dan itu dibahas di post tentang retry dengan exponential backoff dan jitter, sementara post bulkhead membahas cara mencegah satu dependency lambat menahan semua connection.
Retry berlipat ganda antar layer. Jika tiga layer masing-masing mengizinkan tiga percobaan, service terdalam bisa menerima 27 call untuk satu aksi user, padahal ia sudah yang sedang kesulitan. Lakukan retry di satu layer saja, dan hanya untuk operasi yang idempotent.
Apa beda penanganan failure: timeout dan retry atau durable queue?
Caller synchronous memiliki failure-nya sendiri. Ia harus menetapkan timeout, memutuskan apa yang di-retry, dan memutuskan apa yang dilihat user saat callee tidak menjawab. Snippet pertama adalah call NestJS client dengan timeout eksplisit. Tanpa itu kamu bergantung pada default, dan pricing service yang hang akan menggantung checkout yang memanggilnya.
import { HttpService } from "@nestjs/axios";
import { Injectable, ServiceUnavailableException } from "@nestjs/common";
import { firstValueFrom } from "rxjs";
@Injectable()
export class PricingClient {
constructor(private readonly http: HttpService) {}
async quote(serviceId: string): Promise<{ priceIdr: number }> {
try {
// Right: an explicit timeout. Wrong: relying on a default and waiting on a hung callee.
const { data } = await firstValueFrom(
this.http.get("http://pricing:3000/quotes/" + serviceId, { timeout: 800 }),
);
return data;
} catch {
// The caller owns this failure: decide what the cashier sees.
throw new ServiceUnavailableException("Pricing is unavailable, try again");
}
}
}
Publisher menyerahkan masalahnya ke broker. Jika consumer mati, panduan Azure mencatat bahwa pengirim tetap bisa mengirim, dan message diambil saat consumer pulih. Snippet kedua mem-publish event. Caller tidak lagi tahu atau peduli siapa yang bereaksi.
import { Inject, Injectable } from "@nestjs/common";
import { ClientProxy } from "@nestjs/microservices";
import { lastValueFrom } from "rxjs";
@Injectable()
export class PaymentEvents {
constructor(@Inject("EVENT_BUS") private readonly bus: ClientProxy) {}
async paymentCaptured(paymentId: string, amountIdr: number) {
// The receipt, stock and report services react on their own schedule.
// Include an id: delivery is at-least-once, so consumers must de-duplicate on it.
await lastValueFrom(
this.bus.emit("payment.captured", { eventId: paymentId, amountIdr }),
);
}
}
Harganya, delivery biasanya menjadi at-least-once. Panduan Azure menyebut kamu harus menangani message duplikat, baik dengan de-duplication maupun dengan membuat operasi idempotent. Queue adalah buffer yang tahan lama, bukan jaminan bahwa pekerjaan terjadi tepat sekali.
Apa dampak asynchronous messaging terhadap consistency?
Synchronous call kembali saat pekerjaan selesai, jadi baris kode berikutnya boleh mengandalkannya. Setelah publish, pekerjaan baru diserahkan. Service lain akan sebentar tertinggal, dan UI harus jujur soal itu, misalnya menampilkan status pending, bukan status terkonfirmasi. Itulah eventual consistency, dan ia keputusan produk sama besarnya dengan keputusan teknis.
Jebakan klasiknya adalah dual write: commit ke database, lalu publish, dan crash di antara keduanya. State berubah tetapi tidak ada yang diberi tahu. Menulis event ke tabel outbox dalam transaction yang sama dengan perubahan state, lalu mem-publish dari tabel itu, menutup celahnya.
// Wrong: if the process dies after the commit, the payment exists and nobody is told.
await db.transaction((tx) => tx.insert(payments).values(row));
await bus.emit("payment.captured", row);
// Right: the event is written in the SAME transaction; a relay publishes it afterwards.
await db.transaction(async (tx) => {
await tx.insert(payments).values(row);
await tx.insert(outbox).values({
topic: "payment.captured",
payload: JSON.stringify(row),
publishedAt: null, // the relay sets this after a successful publish
});
});
Outbox membuat publish menjadi at-least-once, itulah sebabnya de-duplication di sisi consumer dari bagian sebelumnya bukan pilihan.
Kapan synchronous tepat, dan kapan asynchronous tepat?
Putuskan per interaksi, bukan per sistem. Ajukan satu pertanyaan untuk setiap call: apakah caller butuh hasilnya untuk melanjutkan? Pembagian yang akan saya buat di POS carwash mengikuti pertanyaan itu.
Synchronous cocok saat jawabannya dibutuhkan sekarang:
Membaca harga atau data customer saat menyusun layar checkout.
Memvalidasi voucher sebelum total ditampilkan.
Query apa pun yang errornya harus langsung ditampilkan ke user.
Operasi yang harus berhasil atau gagal bersama sebelum kamu membalas.
Asynchronous cocok saat pekerjaan boleh terjadi setelah balasan:
Side effect seperti mengirim struk atau notifikasi.
Fan-out, saat satu pembayaran memberi makan stok, reporting, dan analytics.
Pekerjaan lama seperti laporan bulanan atau export.
Load levelling, saat queue menyerap lonjakan yang dikuras service hilir dengan kecepatannya sendiri.
Jika kamu masih memilih protocol untuk sisi synchronous, perbandingan gRPC versus REST sebelumnya membahasnya. Untuk sisi asynchronous, post tentang message queue dan tentang pub/sub versus queue membahas kapan competing-consumer queue atau topic fan-out cocok. Dan jika event terasa terlalu berat, post tentang kapan tidak memakai event-driven adalah penyeimbang yang jujur.
Bisakah keduanya digabung dengan response 202 Accepted?
Bisa, dan ini hybrid yang paling berguna. Client mengirim request synchronous, server memvalidasi, menyimpan job, dan langsung mengembalikan 202 Accepted. RFC 9110 mendefinisikannya sebagai diterima untuk diproses tetapi belum selesai, dan menyebut response-nya sengaja noncommittal. RFC juga menyebut response sebaiknya menjelaskan status request saat ini dan menunjuk ke status monitor, yaitu endpoint polling di bawah.
import { Body, Controller, Get, Headers, HttpCode, NotFoundException, Param, Post, Res } from "@nestjs/common";
import type { Response } from "express";
@Controller("reports")
export class ReportsController {
constructor(private readonly reports: ReportsService) {}
@Post()
@HttpCode(202) // accepted for processing, not completed (RFC 9110, 15.3.3)
async create(
@Body() dto: CreateReportDto,
@Headers("idempotency-key") key: string,
@Res({ passthrough: true }) res: Response,
) {
// Insert the job row and enqueue in ONE transaction; same key returns the same job.
const job = await this.reports.enqueue(dto, key);
res.setHeader("Location", "/reports/" + job.id); // the status monitor
res.setHeader("Retry-After", "5"); // suggested seconds before polling
return { id: job.id, status: "queued" };
}
@Get(":id")
async status(@Param("id") id: string) {
const job = await this.reports.find(id);
if (!job) throw new NotFoundException();
// queued | running | done | failed - a 202 promised nothing, so failure must be reportable.
return { id: job.id, status: job.status, error: job.error ?? null, resultUrl: job.resultUrl ?? null };
}
}
Client melakukan polling ke status URL, atau kamu memanggil webhook saat job selesai. Create dan baris job ditulis bersama, sehingga 202 tidak pernah menjadi janji yang tak bisa ditepati server. Tambahkan idempotency key pada POST agar request yang di-retry tidak mengantrekan pekerjaan dua kali.
RFC 9110 eksplisit soal batasnya: HTTP tidak punya fasilitas untuk mengirim ulang status code dari operasi asynchronous. 202 berarti request diterima, bukan akan berhasil, jadi status endpoint harus bisa melaporkan kegagalan.
Beri tag tiap interaksi di design doc: query, command yang butuh hasil, atau side effect. Query tetap synchronous, side effect menjadi event, dan command dengan hasil lambat memakai bentuk 202. Sistemnya akhirnya campuran, dan itu hasil yang benar.
Jangan memilih satu gaya untuk seluruh arsitektur. Simpan synchronous call untuk jawaban yang dibutuhkan caller saat itu juga, dan hitung chain-nya: setiap hop wajib mengalikan availability dan menambah latency. Pindahkan side effect, fan-out, dan pekerjaan lama ke balik message yang durable, terima delivery at-least-once, dan kembalikan 202 dengan status endpoint bila client tetap ingin tahu hasilnya.