Microservices with Spring Boot : Asynchronous Inter-Service Communication using @EnableAsync and @Async
In this article, we will see how two microservices developed using Spring Boot will asynchronously communicate with each other using Spring's @EnableAsync
and @async
What are we going to build?
Use Case:
We will build a user service that creates new users and stores them in an embedded H2 database. We will have a mail service that sends a confirmation mail to the newly created users. We will have a runner service that the client can call to send the user details. The runner service will synchronously call the user service to create a new user and then asynchronously call the mail service to send the mail.
Note: Sending the mail takes some time. If we call the mail service synchronously, then that may take time and keep blocking the main thread and this will result in a delay in response for the client. So, we call the mail service asynchronously, i.e. on a different thread. This results in a quick response to the client and mail are sent later on a different thread.
Build the user service
Note: For this article, we will use maven.
Add the following dependencies :
Spring Web
Lombok
Spring Data JPA
H2 Database
For this article, we are using Spring Boot version 2.7.8 and Java 11.
Click on Generate and open the project in an IDE (IntelliJ, Eclipse, VSCode, etc)
Create a User Entity
Create an entities package and inside it create a User.java class
entities/User.java
import lombok.*;
import javax.persistence.*;
@Entity
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Table(name = "users")
public class User
{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
long id;
String firstName;
String lastName;
String email;
}
Create a JPA Repository for User
Create a package named repositories and create an interface for the user JPA repository.
repositories/UserRepository.java
import com.umang345.userserviceasynccommunicationenableasync.entities.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
Add database properties
Add H2 Database properties and server port in application.properties file
server.port=8081
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
spring.h2.console.path=/h2
Define the methods in the UserService interface
We will create a service layer over the JPA layer. Create a service package and add a UserService interface.
services/UserService.java
import com.umang345.userserviceasynccommunicationenableasync.entities.User;
import org.springframework.stereotype.Service;
@Service
public interface UserService
{
User createUser(User newUser);
}
Implement the UserService interface
We will add an implementation for the UserService interface.
services/UserServiceImpl.java
import com.umang345.userserviceasynccommunicationenableasync.entities.User;
import com.umang345.userserviceasynccommunicationenableasync.repositories.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
public User createUser(User newUser) {
User savedUser = userRepository.save(newUser);
return savedUser;
}
}
Create a DTO for User Response
Create a class UserResponseDto
to wrap and return the response and status of the newly created user.
entities/ UserResponseDto.java
import lombok.*;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Builder
public class UserResponseDto
{
Integer status;
User data;
}
Add the Controller for the User
We will implement a UserController that will expose the endpoints for the CRUD operations.
controllers/UserController.java
import com.umang345.userserviceasynccommunicationenableasync.entities.User;
import com.umang345.userserviceasynccommunicationenableasync.entities.UserResponseDto;
import com.umang345.userserviceasynccommunicationenableasync.services.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController
{
@Autowired
private UserService userService;
@PostMapping
public ResponseEntity<?> createUser(@RequestBody User newUser) {
User createdUser = userService.createUser(newUser);
UserResponseDto response = UserResponseDto.builder()
.status(HttpStatus.CREATED.value())
.data(createdUser)
.build();
return ResponseEntity.ok().body(response);
}
}
pom.xml
The pom.xml for the user service must contain the following dependencies :
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
With this, we complete our user service.
Build the Mail service
Now we will build the mail service that is asynchronously called by the runner service.
Go to https://start.spring.io/
Add the following dependencies :
Spring Web
Lombok
For this article, we are using Spring Boot version 2.7.8 and Java 11.
Click on Generate and open the project in an IDE (IntelliJ, Eclipse, VSCode, etc)
Add the mail dependency
Add the following dependency in the pom.xml file.
<!-- https://mvnrepository.com/artifact/com.sun.mail/javax.mail -->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
pom.xml
The pom.xml of the mail service should contain the following dependencies.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/com.sun.mail/javax.mail -->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
Create the User entity
We will create the same user entity for the runner class by adding the database properties.
entities/User.java
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User
{
long id;
String firstName;
String lastName;
String email;
}
Define the server post and authentication properties
server.port=8082
authentication.username=<Sender Email Id>
authentication.password=<Sender Password>
Note: Replace the above properties with the email credentials from which you want to send the mail.
Add an interface for the Mail Service.
Create a MailService
interface and define the method to send the mail.
services/MailService.java
import org.springframework.stereotype.Service;
@Service
public interface MailService
{
void sendMail(String message, String subject, String to, String from);
}
Add the implementation for the MailSerice interface.
Create a MailServiceImpl
class them implements the MailService
interface.
services/MailServiceImpl.java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Properties;
@Service
public class MailServiceImpl implements MailService
{
@Value("${authentication.username}")
private String AUTHENTICATION_USERNAME;
@Value("${authentication.password}")
private String AUTHENTICATION_PASSWORD;
@Override
public void sendMail(String message, String subject, String to, String from) {
//Variable for gmail
String host="smtp.gmail.com";
//get the system properties
Properties properties = System.getProperties();
System.out.println("PROPERTIES "+properties);
//setting important information to properties object
//host set
properties.put("mail.smtp.host", host);
properties.put("mail.smtp.port","465");
properties.put("mail.smtp.ssl.enable","true");
properties.put("mail.smtp.auth","true");
//Step 1: to get the session object..
Session session=Session.getInstance(properties, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(AUTHENTICATION_USERNAME,
AUTHENTICATION_PASSWORD);
}
});
session.setDebug(true);
//Step 2 : compose the message [text,multi media]
MimeMessage m = new MimeMessage(session);
try {
//from email
m.setFrom(from);
//adding recipient to message
m.addRecipient(Message.RecipientType.TO, new InternetAddress(to));
//adding subject to message
m.setSubject(subject);
//adding text to message
m.setText(message);
//send
//Step 3 : send the message using Transport class
Transport.send(m);
System.out.println("Sent success...................");
}catch (Exception e) {
e.printStackTrace();
}
}
}
Add the Mail Controller
Create a class MailController
that contains the endpoint to send the mail.
controllers/MailController.java
import com.umang345.mailserviceasynccommunicationenableasync.entities.User;
import com.umang345.mailserviceasynccommunicationenableasync.services.MailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/mail")
public class MailController
{
@Autowired
private MailService mailService;
@Value("${authentication.username}")
private String FROM;
@PostMapping
public Map<String,Object> sendMail(@RequestBody User user) {
StringBuilder message = new StringBuilder();
message.append("Hi ");
message.append(user.getFirstName());
message.append(", Your new account is created Successfully");
String subject = "New Account Created";
mailService.sendMail(
message.toString(),
subject,
user.getEmail(),
FROM
);
Map<String,Object> response = new HashMap<>();
response.put("status",true);
return response;
}
}
With this, we complete our mail service.
Build the Runner Service
Now we will build the runner service that is directly called by the client.
Go to https://start.spring.io/
Add the following dependencies :
Spring Web
Lombok
For this article, we are using Spring Boot version 2.7.8 and Java 11.
Click on Generate and open the project in an IDE (IntelliJ, Eclipse, VSCode, etc)
Create the User entity
We will create the same user entity for the runner class by adding the database properties.
entities/User.java
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User
{
long id;
String firstName;
String lastName;
String email;
}
Create a DTO for User Response
Create a class UserResponseDto
to receive the response and status of the newly created user from the user service.
entities/ UserResponseDto.java
import lombok.*;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Builder
public class UserResponseDto
{
Integer status;
User data;
}
Create a DTO for Runner Response
Create a class RunnerResponseDto
to wrap and return the response to the client
entities/RunnerResponseDto.java
import lombok.*;
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class RunnerResponseDto
{
User user;
String message;
}
Add a Bean for the RestTemplate
Create a MyConfiguration
class to add a bean for RestTemplate
configs/MyConfiguration.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class MyConfiguration
{
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
Set the server port and URL properties
In the application.properties
file, set the server port and service urls.
server.port=8080
url.user-service=http://localhost:8081/users
url.mail-service=http://localhost:8082/mail
Create a utility class for sending the mail
Create a class MailUtil
which contains a method to call the endpoint of the mail service to send the mail.
util/MailUtil.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
public class MailUtil {
@Autowired
private RestTemplate restTemplate;
public void sendMail(String url, HttpMethod httpMethod, HttpEntity<?> httpEntity, Class<?> c)
{
restTemplate.exchange(url
,httpMethod
,httpEntity
,c);
}
}
pom.xml
The pom.xml of the runner service should contain the following dependencies.
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
Create the Runner Controller
Create a class RunnerController
that contains an endpoint that the user calls to send the user details to create a new User.
controllers/RunnerController.java
import com.umang345.runnerserviceasynccommunicationenableasync.entities.User;
import com.umang345.runnerserviceasynccommunicationenableasync.entities.RunnerResponseDto;
import com.umang345.runnerserviceasynccommunicationenableasync.entities.UserResponseDto;
import com.umang345.runnerserviceasynccommunicationenableasync.util.MailUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("/simulate/users")
public class RunnerController
{
@Autowired
private RestTemplate restTemplate;
@Autowired
private MailUtil mailUtil;
@Value("${url.user-service}")
private String userServiceUrl;
@Value("${url.mail-service}")
private String mailServiceUrl;
@PostMapping
public ResponseEntity<?> createUser(@RequestBody User newUser){
ResponseEntity<UserResponseDto> response = null;
try {
long tm1 = System.currentTimeMillis();
response = restTemplate.exchange(userServiceUrl,HttpMethod.POST,new HttpEntity<>(newUser), UserResponseDto.class);
long tm2 = System.currentTimeMillis();
System.out.println("User created in : "+((tm2-tm1)/1000.0));
if(response.getBody().getStatus() != HttpStatus.CREATED.value())
{
throw new Exception("Error while creating user");
}
User createdUser = response.getBody().getData();
RunnerResponseDto responseDto = RunnerResponseDto
.builder()
.user(createdUser)
.message("User created successfully. Mail will be sent shortly")
.build();
mailUtil.sendMail(mailServiceUrl
,HttpMethod.POST
,new HttpEntity<>(createdUser)
,Void.class);
return ResponseEntity.status(HttpStatus.OK).body(responseDto);
}catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
}
}
}
Note: Till now we have not added or configured the code for asynchronous communication.
Let us test out our runner service when both services are called synchronously
We will use Postman. Start all the services. The runner, user and mail service will be running on ports 8080, 8081 and 8082 respectively.
Will will hit the endpoint
POST
http://localhost:8080/simulate/users
{
"firstName":"pawan",
"lastName":"Agarwal",
"email":<Enter a Valid Email Id>
}
Note: Replace a valid mail in the email field.
Note: the response took more than 9 seconds.
Now we will enable asynchronous communication for the mail service.
First,
Go the the RunnerServiceAsyncCommunicationEnableAsyncApplication
class and add the @EnableAsync
annotation.
RunnerServiceAsyncCommunicationEnableAsyncApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class RunnerServiceAsyncCommunicationEnableAsyncApplication {
public static void main(String[] args) {
SpringApplication.run(RunnerServiceAsyncCommunicationEnableAsyncApplication.class, args);
}
}
Add the @Async
annotation
In the MailUtil
class, add the @Async
annotation to the sendMail
method.
util/MathUtil.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
public class MailUtil {
@Autowired
private RestTemplate restTemplate;
@Async
public void sendMail(String url, HttpMethod httpMethod, HttpEntity<?> httpEntity, Class<?> c)
{
restTemplate.exchange(url
,httpMethod
,httpEntity
,c);
}
}
Now, let's test our app again.
Restart all the services.
Will will hit the endpoint
POST
http://localhost:8080/simulate/users
{
"firstName":"Aman",
"lastName":"Agarwal",
"email":<Enter a Valid Email Id>
}
Note: Replace a valid mail in the email field.
Note: After making the call asynchronously, the response took less than a second to return to the client and mail was sent on a different thread.
Find the source code of the project on GitHub.
Do star the repository to access the source code of all the articles.
I hope you found the article useful.
Let's connect :
Happy Coding :)