Strategi Komunikasi Antar Service

Mon. Jun 29th, 2026 10:42 PM6 mins read
Strategi Komunikasi Antar Service
Source: Gemini Nano Banana Pro - server communication

Ketika membangun aplikasi yang terintegrasi dengan service lain kita perlu memilih strategi yang dilakukan untuk berkomunikasi atau bertukar pesan antar service. Biasanya pada Microservices strategi yang paling umum untuk dilakukan adalah Direct Call menggunakan HTTP. Namun, pendekatan ini belum tentu cocok untuk semua kasus. Ada beberapa pilihan strategi komunikasi yang bisa dipertimbangkan.

Direct Call⌛

Ini adalah strategi yang paling gampang diimplementasi karena kita langsung panggil endpoint service yang dituju lalu tunggu responsenya. Ini ga perlu infrastruktur tambahan sebagai perantara. Kalau terjadi error pada service yang dituju atau servicenya ga bisa dihubungi, bisa langsung dihandle saat itu juga. Kekurangannya, ini blocking😴. Setelah dipanggil kita harus menunggu response dari server yang dituju. Kalau responsenya lama maka bakal berdampak juga pada resource dari service yang memanggil karena harus stand by nungguin hasilnya. Secara performa ini menambah latency dari server yang memanggil. Jika komunikasi antar server banyak, maka akan semakin berat beban resource yang digunakan dan semakin lambat juga latencynya. Ini ga cocok buat fitur yang prosesnya lambat dan bisa dieksekusi independen karena akan berdampak pada latency service yang memanggil. Ini cocoknya pada fitur yang memang bergantung pada response dari service lain. Misalnya pada pemesanan barang kita perlu call inventory service untuk cek stok. Untuk hal ini kita memang harus nungguin response dari inventory service untuk mendapatkan informasi terkait stok.

Copy
public void createOrder(){
	//call other service synchronously and wait for response
	try(HttpClient client = HttpClient.newHttpClient()){
		HttpRequest request = HttpRequest.newBuilder().GET().uri(new URI("http://localhost:8080/stock/product123")).build();
		HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
		System.out.println("Status Code: " + response.statusCode());
		System.out.println("Body: " + response.body());
	} catch(Exception e){
		//handle exception here
	}

	doOtherThings(); //executed after previous response received
}

Fire & Forget🔥

Ini juga mirip seperti sebelumnya. Bedanya, kita ga perlu nungguin responsenya. Sesuai namanya, setelah dipanggil langsung dilupakan. Kita memanggil service lain secara paralel tanpa perlu tahu responsenya seperti apa. Setelah dipanggil, flownya langsung lanjut ke flow selanjutnya. Meskipun response dari server yang dituju lambat atau service yang dipanggil banyak, ini ga ngaruh ke latency service yang memanggil. Distribusinya At-most-once, bisa berhasil maksimal sekali, tapi bisa ga terkirim sama sekali saat service yang dituju ga bisa dihubungi. Kekurangannya, ini tetap membebani resource dari service yang memanggil. Kita hanya memindahkan HTTP call ke background process ke luar flow tapi masih di service yang sama. Jika server yang dipanggil banyak, beban resource masih ditanggung service yang memanggil. Selain itu, misalkan service tujuan error atau ga bisa dihubungi, maka komunikasinya akan putus begitu saja karena flownya sudah lanjut ke flow selanjutnya💔. Ini ga cocok buat fitur penting yang harus dihandle jika ada error. Misalnya saat service buat notifikasi user ga bisa dihubungi, maka notifikasi ga akan pernah terkirim. Ini cocoknya buat fitur yang opsional, jika gagal dipanggil ga masalah, dan flownya sangat sederhana sehingga ga banyak makan resource. Misalnya saat mencatat analitik dari halaman yang dibuka user untuk kebutuhan internal, di sini kita hanya meng-increment counter ke service analitik tanpa perlu tahu responsenya apa, dan kalau gagal bukan masalah besar.

Copy
public void createOrder(){
	//call other service asynchronously
	CompletableFuture.runAsync(() -> {
		try(HttpClient client = HttpClient.newHttpClient()){
			HttpRequest request = HttpRequest.newBuilder().POST().uri(new URI("http://localhost:8080/analytics/page/count")).build();
			HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
			System.out.println("Status Code: " + response.statusCode());
			System.out.println("Body: " + response.body());
		} catch(Exception e){
			//handle exception here
		}
	});

	doOtherThings(); //executed immediately without waiting previous response
}

Publisher & Subscriber📬

Ini masih mirip Fire & Forget. Distribusinya juga At-most-once. Hanya saja, kali ini kita butuh infrastruktur tambahan seperti Message Broker sebagai perantara. Misalnya kita menggunakan Redis PubSub. Service yang mengirim request disebut Publisher, sedangkan yang menerima dan memproses request disebut Subscriber. Saat ingin mengirim request ke service lain, kita akan kirim request ke Redis PubSub. Setelah itu langsung lanjut ke flow selanjutnya tanpa perlu tahu responsenya seperti apa sehingga ga ngaruh ke latency. Redis PubSub akan langsung meneruskan request tersebut ke service tujuan yang menjadi Subscriber. Di sini beban resource udah ga ditanggung oleh service yang memanggil meskipun ada banyak komunikasi antar server. Request yang dikirim hanya numpang lewat menggunakan Redis sehingga lebih hemat resource🚶. Kekurangannya, komunikasinya masih akan putus begitu saja jika service yang dituju error atau ga bisa dihubungi. Sama seperti Fire & Forget, ini cocoknya buat fitur yang jika gagal dipanggil ga masalah.

Publisher
Copy
public class PageCounterPublisher{

	private final RedisOperations<String, Object> redisTemplate;

	public void publish(Message message) {
		redisTemplate.convertAndSend("pageCount", message);
		doOtherThings();
	}

}
Subscriber
Copy
public class PageCounterSubscriber implements MessageListener{

	@Override
	public void onMessage(Message message, byte[] pattern){
		//receive message & do incremental counter
	}
}
Subscriber Config
Copy
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, PageCounterSubscriber subscriber) {

	RedisMessageListenerContainer container =
			new RedisMessageListenerContainer();
	container.setConnectionFactory(connectionFactory);

	container.addMessageListener(
			subscriber,
			new ChannelTopic("pageCount")
	);

	return container;
}

Message Queue📩

Contoh infrastrukturnya adalah RabbitMQ. Saat ingin mengirim request ke service lain, kita akan kirim request ke RabbitMQ dan flownya lanjut ke flow selanjutnya. Di sini request yang dikirim ga hanya numpang lewat, melainkan akan disimpan terlebih dahulu ke antrian RabbitMQ. Lalu RabbitMQ akan menghubungi server yang dituju. Kalau ga bisa dihubungi, maka request akan ditunda dan masih tersimpan di antrian. Jika sudah bisa dihubungi, maka request akan diteruskan ke service tujuan dan dihapus dari antrian. Ini menyelesaikan masalah dari pendekatan di atas saat service yang dituju ga bisa dihubungi sehingga komunikasi ga terputus begitu saja dan akan dilanjutkan saat service yang dituju sudah bisa dihubungi🛜. Distribusinya At-least-once, yaitu request akan diproses minimal sekali, tapi ada kemungkinan akan terproses lebih dari sekali saat ada error pada Consumer sehingga perlu handle idempotency dengan baik agar request ga terproses duplikat. Kekurangannya, pendekatan ini sifatnya Push-based, yaitu RabbitMQ yang akan mengirim request ke server yang dituju. Misalkan request yang dikirim terlalu banyak, maka server yang dituju bisa ga kuat memprosesnya karena RabbitMQ akan mengirim request terus-terusan selama masih ada antrian, ga peduli si server yang dituju ini kuat atau nggak. Misalnya ada 1juta antrian, sedangkan Subscriber hanya kuat melayani 100, maka Subscriber akan dipaksa handle 1juta request sampai ga kuat. Ini dikenal dengan istilah Backpressure. Ini ga cocok pada fitur yang trafiknya susah diprediksi karena bisa memberatkan server yang dituju. Misalkan pada fitur download laporan pajak, pada tanggal tertentu bisa aja fitur itu rame diakses user dan RabbitMQ akan terus-terusan mengirim request ga peduli seberapa kuat service tersebut. Sebaliknya, ini cocok pada fitur yang trafiknya bisa diprediksi seperti email welcome setelah pendaftaran user.

Send Messsage
Copy
@RequiredArgsConstructor
public class MessageProducer{

	private final RabbitTemplate rabbitTemplate;

	public void sendMessage(String message){
		rabbitTemplate.convertAndSend("welcomeEmail", "", message);
		doOtherThings();
	}

}
Receive Message
Copy
public class EmailConsumer{

	@RabbitListener(queues = "welcomeEmail")
	public void receiveMessage(String message){
		//process email here
	}
}

Event Streaming (Producer & Consumer)👋

Contoh infrastrukturnya adalah Kafka, RabbitMQ Streams, dan Redis Streams. Flownya mirip dengan strategi di atas. Service yang mengirim request disebut Producer. Service yang menerima dan memproses request disebut Consumer. Di sini request akan disimpan ke perantara, misalnya Redis Streams sehingga request ga akan hilang meskipun Consumer sedang ga bisa berkomunikasi dan request bisa diproses lagi saat Consumer sudah normal kembali. Bedanya, ini sifatnya Pull-based, yaitu Consumer yang menjemput data request ke Redis. Jadi, setelah request dikirim ke Redis, request akan disimpan ke dalam Stream. Lalu Consumer akan menjemput request yang tersimpan & belum diproses untuk dipindahkan ke PEL (Pending Entries List) kemudian dieksekusi. Setelah diproses, maka Consumer perlu mengubah statusnya menjadi “Acknowledge” dan dikeluarkan dari PEL sebagai tanda bahwa request sudah berhasil diproses. Request yang tersimpan ga terhapus otomatis sehingga kalau ada error saat diproses bisa di-trigger ulang. Di sini Consumer yang bebas menjemput seberapa banyak request yang mau diproses, bukan pasrah aja menerima semua request dari antrian sehingga aman dari Backpressure. Misalkan ada 1juta antrian dan Consumer hanya kuat handle 100, maka 1juta antrian itu tersimpan doang di Stream. Consumer hanya akan menjemput request tiap 100 data, ga semuanya sekaligus. Jaman sekarang strategi ini cukup populer pada Event Driven Architecture karena komunikasinya ga gampang terputus, jika terjadi masalah bisa di-trace dengan mudah lewat request yang tersimpan, bahkan bisa trigger untuk diulang karena requestnya ga dihapus otomatis😎. Distribusinya juga At-least-once dan perlu handle idempotency. Kekurangannya, ini butuh resource ekstra untuk menyimpan data request pada perantara karena semua request disimpan di sana dan ga dihapus otomatis. Ini ga cocok untuk hal-hal simple. Cocoknya ini untuk hal-hal kompleks, lambat, dan peluang kegagalannya harus minim seperti payment, generate report, kirim notifikasi penting, dan sebagainya.

Producer
Copy
@RequiredArgsConstructor
public class ReportGenerationProducer{

	private final StringRedisTemplate redisTemplate;

	public void generate(Report report) {
		Map<String, String> payload = new HashMap<>();
		payload.put("reportId", String.valueOf(report.getId()));

		redisTemplate.opsForStream()
				.add("ReportGenerationStream", payload);
	}

}
Consumer
Copy
@RequiredArgsConstructor
public class ReportGenerationConsumer implements StreamListener<String, MapRecord<String, String, String>>{

	private final StringRedisTemplate redisTemplate;

	@Override
	public void onMessage(MapRecord<String, String, String> message){
		try{
			process(message);
			redisTemplate.opsForStream().acknowledge(
					"ReportGenerationGroup",
					message
			);
		} catch(Exception e){
			e.printStackTrace();
		}
	}
}

Verdict

Itulah 5 strategi untuk komunikasi antar service. Yang paling gampang adalah dengan Direct Call, tapi komunikasinya jadi blocking, resource ditanggung service yang memanggil, dan request bisa hilang di tengah jalan. Yang paling kompleks adalah menggunakan Event Streaming, tapi dengan ini kita terhindar dari Backpressure, komunikasinya non-blocking, resource ditanggung perantara, dan request aman tersimpan tanpa takut hilang di tengah jalan. Semuanya punya kelebihan dan kekurangan masing-masing. Tinggal disesuaikan aja berdasarkan kasus yang dihadapi.

© 2026 · Ferry Suhandri