Java Microservices Architecture: Inter-Service Communication

The Java Trail
8 min readSep 5, 2023

Inter-Service Communication in Monolithic Applications:

In a monolithic architecture, all components and modules of the application are tightly integrated into a single codebase, and they run within the same process or on the same machine. Since all components are part of the same application, there is typically no need for inter-service communication or remote calls for internal business functions. Communication between components is achieved through language-level method calls or simple function calls within the same codebase.

// Monolithic application with internal method calls
public class MonolithicApp {
public void performBusinessLogic() {
// Call a method within the same application
ComponentA.doSomething();
ComponentB.processData();
}
}

class ComponentA {
public static void doSomething() {
// Business logic for Component A
}
}

class ComponentB {
public static void processData() {
// Business logic for Component B
}
}

In this monolithic example, the MonolithicApp class directly calls methods within ComponentA and ComponentB. Since all components are part of the same application, there is no need for network communication, and method calls are straightforward.

Migrating From Monolithic to Microservices

Inter-Service Communication in Microservices-based Applications:

In contrast, a microservices-based architecture decomposes the application into smaller, independently deployable services that run on separate machines or containers. Each service is responsible for specific business capabilities or functions. In this distributed environment, inter-service communication becomes crucial for services to collaborate and deliver the application’s overall functionality.

One-to-one — Each client request is processed by exactly one service instance.

There are the following kinds of one-to-one interactions:

  • Request/response — A client makes a request to a service and waits for a response. The client expects the response to arrive in a timely fashion. In a thread-based application, the thread that makes the request might even block while waiting.
  • REST is an IPC mechanism- that (almost always) uses HTTP. A key concept in REST is a resource, which typically represents a business object such as a Customer or Product, or a collection of business objects. For example, a GET request returns the representation of a resource, which might be in the form of an XML document or JSON object. A POST request creates a new resource and a PUT request updates a resource. To quote Roy Fielding, the creator of REST:

The following diagram shows one of the ways that the taxi-hailing application might use REST.

In this scenario, we have two microservices: the Trip Management service and the Passenger Management service.

  1. The passenger’s smartphone initiates communication by making a POST request to the Trip Management service to request a trip.
  2. The Trip Management service, in turn, communicates with the Passenger Management service to verify the passenger’s authorization before creating the trip.
  3. Once verified, the Trip Management service responds with a 201 status code to confirm the trip request.

Step 1: Set Up Eureka Server

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>


//Create a Eureka Server application class with the @EnableEurekaServer annotation to enable Eureka server functionality.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}

Step 2: Create the Eureka Clients

Passenger Management Service: provides passenger data and authorizes passengers.

In your pom.xml, add the necessary dependencies for Spring Boot and Eureka Client:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Other dependencies as needed -->
</dependencies>

In your application.properties (or application.yml) file, specify the application name and the location of the Eureka server:

spring.application.name=passenger-management-service
eureka.client.service-url.default-zone=http://eureka-server-host:port/eureka/
//Passenger.java (Model Class):
public class Passenger {
private long id;
private String name;
private boolean authorized;

// getters and setters
}
//PassengerController.java (Controller):
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/passengers")
public class PassengerController {

@GetMapping("/{id}")
public Passenger getPassengerById(@PathVariable long id) {
// Simulate fetching passenger data from a database or service
// instead you may use a repository to fetch data from datastore
Passenger passenger = new Passenger();
passenger.setId(id);
passenger.setName("John Doe");
passenger.setAuthorized(true);
return passenger;
}
}

Trip Management Service: receives trip requests, communicates with the Passenger Management service to verify authorization, and then creates trips.

In your pom.xml, add the necessary dependencies for Spring Boot and Eureka Client:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Other dependencies as needed -->
</dependencies>

In your application.properties (or application.yml) file, specify the application name and the location of the Eureka server:

spring.application.name=trip-management-service
eureka.client.service-url.default-zone=http://eureka-server-host:port/eureka/
//Trip.java (Model Class):
public class Trip {
private long id;
private String origin;
private String destination;

// getters and setters
}
//TripController.java (Controller):
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/trips")
public class TripController {

private final String passengerServiceUrl = "http://passenger-management-service/passengers";

@PostMapping
public ResponseEntity<Trip> createTrip(@RequestBody Trip tripRequest) {
// Make a GET request to Passenger Management service to verify authorization
RestTemplate restTemplate = new RestTemplate();
Passenger passenger = restTemplate.getForObject(passengerServiceUrl + "/" + tripRequest.getId(), Passenger.class);

if (passenger != null && passenger.isAuthorized()) {
// Authorization successful, create the trip
Trip trip = new Trip();
trip.setId(12345); // Replace with a unique trip ID
trip.setOrigin(tripRequest.getOrigin());
trip.setDestination(tripRequest.getDestination());

// Simulate saving the trip to a database or service

return new ResponseEntity<>(trip, HttpStatus.CREATED);
} else {
// Authorization failed, return a 401 Unauthorized response
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
}
}
}
  • Notification (a.k.a. a one-way request) — A client sends a request to a service but no reply is expected or sent.
import org.springframework.web.client.RestTemplate;

public class NotificationExample {
public static void main(String[] args) {
RestTemplate restTemplate = new RestTemplate();
String serviceUrl = "https://api.example.com/notification"; // Replace with your service URL

// Send a POST request (or any HTTP method) without expecting a response
restTemplate.postForLocation(serviceUrl, null);

System.out.println("Request sent, no response expected.");
}
}
  • Request/async response — A client sends a request to a service, which replies asynchronously. The client does not block while waiting and is designed with the assumption that the response might not arrive for a while.
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.AsyncRestTemplate;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;

public class RequestAsyncResponseExample {
public static void main(String[] args) {
AsyncRestTemplate asyncRestTemplate = new AsyncRestTemplate();
String serviceUrl = "https://api.example.com/async-data"; // Replace with your service URL

// Send a GET request asynchronously
ListenableFuture<ResponseEntity<String>> futureResponse = asyncRestTemplate.getForEntity(serviceUrl, String.class);

// Register a callback for handling the response when it arrives
futureResponse.addCallback(new ListenableFutureCallback<ResponseEntity<String>>() {
@Override
public void onSuccess(ResponseEntity<String> result) {
System.out.println("Async response received: " + result.getBody());
}

@Override
public void onFailure(Throwable ex) {
System.err.println("Async request failed: " + ex.getMessage());
}
});

// Continue with other work without blocking
System.out.println("Continuing with other tasks while waiting for the async response...");
}
}

One-to-many — Each request is processed by multiple service instances.

  • Publish/subscribe — A client publishes a notification message, which is consumed by zero or more interested services.
  • Publish/async responses — A client publishes a request message, and then waits a certain amount of time for responses from interested services.

An example scenario implementing all these communication:

  1. Passenger Service (Notification):

PassengerController.java:

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/passengers")
public class PassengerController {

@PostMapping("/request-pickup")
public ResponseEntity<String> requestPickup() {
// Logic to process pickup request
// ...

// Notify the Trip Management service about the pickup request
// This can be done asynchronously via Kafka or another messaging system
// Example Kafka producer code:
kafkaProducer.send("pickup-requested", "Passenger requested a pickup");

return ResponseEntity.ok("Pickup requested successfully");
}
}

2. Trip Management Service (Request/Response and Publish/Subscribe):

TripManagementProducer.java:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

@Service
public class TripManagementProducer {

private final KafkaTemplate<String, String> kafkaTemplate;

@Autowired
public TripManagementProducer(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}

public void createTrip(String tripId) {
// Logic to create the trip
// ...

// Notify the Dispatcher about the trip creation
kafkaTemplate.send("trip-created", "Trip created: " + tripId);
}
}

3. Dispatcher Service (Subscribe and Send Messages):

DispatcherConsumer.java:

import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;

@Service
public class DispatcherConsumer {

@KafkaListener(topics = "trip-created", groupId = "dispatcher-group")
public void consumeTripCreatedMessage(String message) {
// Logic to handle Trip Created message and find an available driver
// ...

// Notify Passenger Management and Driver Management about the driver proposal
notifyPassengerManagement("Driver Proposed: " + message);
notifyDriverManagement("Driver Proposed: " + message);
}

private void notifyPassengerManagement(String notification) {
// Logic to send notification to Passenger Management
// ...
}

private void notifyDriverManagement(String notification) {
// Logic to send notification to Driver Management
// ...
}
}

3. Passenger Management Service(Kafka Consumers):

PassengerManagementConsumer.java:

import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

@Service
public class DriverManagementConsumer {

private final KafkaTemplate<String, String> kafkaTemplate;

public DriverManagementConsumer(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}

@KafkaListener(topics = "driver-proposed-driver", groupId = "driver-management-group")
public void consumeDriverProposalMessage(String message) {
// Logic to handle Driver Proposed message from Dispatcher
// ...

// Send a notification to the Notification Service
sendNotificationToNotificationService("Driver received proposal: " + message);
}

private void sendNotificationToNotificationService(String notification) {
// Send the notification to the Notification Service via Kafka
kafkaTemplate.send("notifications", notification);
}
}

4. Driver Management Service (Kafka Consumers):

DriverManagementConsumer.java:

import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

@Service
public class DriverManagementConsumer {

private final KafkaTemplate<String, String> kafkaTemplate;

public DriverManagementConsumer(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}

@KafkaListener(topics = "driver-proposed-driver", groupId = "driver-management-group")
public void consumeDriverProposalMessage(String message) {
// Logic to handle Driver Proposed message from Dispatcher
// ...

// Send a notification to the Notification Service
sendNotificationToNotificationService("Driver received proposal: " + message);
}

private void sendNotificationToNotificationService(String notification) {
// Send the notification to the Notification Service via Kafka
kafkaTemplate.send("notifications", notification);
}
}

5. Notification Service:

NotificationConsumer.java:

import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;

@Service
public class NotificationConsumer {

@KafkaListener(topics = "notifications", groupId = "notification-group")
public void receiveNotification(String notification) {
// Logic to process received notifications
// ...

// For simplicity, just print the notification for demonstration
System.out.println("Received notification: " + notification);
}
}

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — -

In Microservice Architecture, we can classify our inter-service communication into two approaches like the following:

  • Synchronous — The client expects a timely response from the service and might even block while it waits.
  • Asynchronous — The client doesn’t block while waiting for a response, and the response, if any, isn’t necessarily sent immediately.

--

--

The Java Trail

Scalable Distributed System, Backend Performance Optimization, Java Enthusiast. (mazumder.dip.auvi@gmail.com Or, +8801741240520)