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

Go to start.spring.io

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

application.properties

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

application.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.

application.properties

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 :)