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 server lain kita perlu memilih strategi yang dilakukan untuk berkomunikasi atau bertukar pesan antar service. Biasanya pada microservices pendekatan yang paling umum dilakukan adalah dengan menggunakan direct call menggunakan HTTP. Namun, pendekatan ini belum tentu cocok untuk semua kasus. Ada beberapa pilihan pendekatan yang bisa dilakukan.

Direct Call

Ini adalah pendekatan yang paling gampang diimplementasi karena kita tinggal panggil endpoint service yang dituju lalu tunggu responsenya. Ini ga perlu infrastruktur tambahan, hanya butuh koneksi jaringan ke server yang dituju. Kalau terjadi error pada service yang dituju atau servicenya ga bisa dihubungi, bisa langsung dihandle saat itu juga. Kekurangannya, ini artinya 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 kalau eksekusinya lambat 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
//call other service 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 call, flownya langsung lanjut ke flow selanjutnya. Meskipun response dari server yang dituju lambat, ini ga ngaruh ke latency service yang memanggil. Jika server yang dipanggil banyak ga ngaruh ke latency. Komunikasinya At-most-once, bisa berhasil maksimal sekali, tapi bisa gagal ga terkirim sama sekali. 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 tanpa tahu response dari pemanggilan tersebut. Ini ga cocok buat fitur penting yang harus dihandle jika ada error. Misalkan service buat generate report error saat dipanggil, maka report ga akan pernah digenerate. 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
//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. Komunikasinya juga At-most-once. Hanya saja, kali ini kita butuh infrastruktur tambahan sebagai perantara. Misalnya kita menggunakan Redis PubSub sebagai perantara. Service yang mengirim request disebut Publisher, sedangkan yang menerima dan memproses request disebut Subscriber. Saat ingin mengirim request ke service lain, kita akan call 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. Prosesnya sudah terpusat lewat Redis. 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. Di sini request yang dikirim ga hanya numpang lewat, melainkan akan disimpan terlebih dahulu ke antrian RabbitMQ. Lalu RabbitMQ akan mencoba berkomunikasi ke server yang dituju. Kalau ga bisa dihubungi, maka request akan ditunda dan masih tersimpan di antrian. Jika sudah bisa dihubungi, maka request akan dikirimkan 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. 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 menghandlenya 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 hingga 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();
	}

}

Event Streaming

Contoh infrastrukturnya adalah Kafka, Rabbit Streaming, dan Redis Streams. Service yang mengirim request disebut Producer. Service yang menerima dan memproses request disebut Consumer. Dengan perantara ini, request akan disimpan ke perantara, misalnya Redis Streams. Bedanya, ini sifatnya Pull-Based, yaitu Consumer yang meminta data ke Redis. Jadi, setelah request dikirim ke Redis, request akan disimpan dengan status Pending. Lalu Consumer akan meminta request dengan status Pending ke Redis dan statusnya berubah menjadi Processing. Setelah diproses, maka Consumer akan mengubah statusnya menjadi Acknowledge sebagai tanda bahwa request sudah berhasil diproses. Di sini Consumer yang bebas menentukan 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 database perantara. Consumer hanya akan memproses tiap 100 data, ga semuanya sekaligus. Jaman sekarang pendekatan ini cukup populer karena komunikasi ga gampang terputus, jika terjadi masalah bisa di-trace dengan mudah, bahkan bisa diulang karena requestnya ga dihapus otomatis. Kekurangannya, ini sifatnya At-least-once, yaitu request akan diproses minimal sekali, tapi ada kemungkinan akan terproses lebih dari sekali sehingga lebih kompleks implementasinya dan perlu handle idempotency dengan baik agar request ga terproses duplikat. Selain itu, ini juga makan resource karena semua request disimpan dan hanya berganti status setelah diproses tanpa dihapus otomatis. Ini ga cocok untuk hal-hal simple karena butuh resource untuk menyimpan request. Cocoknya ini untuk hal-hal kompleks, lambat, dan kegagalannya harus minimal seperti payment, generate report, kirim notifikasi penting, dan sebagainya.

Producer
Copy
@RequiredArgsConstructor
public class ReportGenerationProducer{

	private final StringRedisTemplate redisTemplate;

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

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

}
Consumer
Copy
@RequiredArgsConstructor
public class TicketBookConsumer 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, request bisa hilang, dan ga cocok untuk hal-hal independen. Yang paling kompleks adalah menggunakan Event Streaming tapi dengan ini kita terhindar dari Backpressure, komunikasinya non-blocking, resource dikelola terpusat, 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