Microservices with Spring Boot : Asynchronous Inter-Service Communication using RabbitMQ

In this article, we will see how two microservices developed using Spring Boot will asynchronously communicate with each other using Rabbit MQ

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 publish the message to RabbitMQ which shall be consumed by the mail service.

Prerequisites

Docker (for running RabbitMQ instance on the machine)

Run the RabbitMQ instance on the docker

run -d --hostname rabbit-mq-instance --name rabbit-1 -p 15672:15672 -p 5672:5672 rabbitmq:3-management

Open localhost:15672 in the browser to see the RabbitMQ dashboard

Username : guest
Password : guest

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.userserviceasyncrabbitmq.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.userserviceasyncrabbitmq.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.userserviceasyncrabbitmq.entities.User;
import com.umang345.userserviceasyncrabbitmq.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.userserviceasyncrabbitmq.entities.User;
import com.umang345.userserviceasyncrabbitmq.entities.UserResponseDto;
import com.umang345.userserviceasyncrabbitmq.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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@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

  • Spring for RabbitMQ

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.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</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>

spring.rabbitmq.addresses = localhost:5672

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.mailserviceasyncrabbitmq.config.UserMessageQueueConfig;
import com.umang345.mailserviceasyncrabbitmq.entities.UserMailQueueMessage;
import com.umang345.mailserviceasyncrabbitmq.services.MailService;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/mail")
public class MailController
{
    @Autowired
    private MailService mailService;

    @Value("${authentication.username}")
    private String FROM;

    @RabbitListener(queues = UserMessageQueueConfig.USER_QUEUE_NAME)
    public void listener(UserMailQueueMessage queueMessage)
    {
        System.out.println(queueMessage);

        StringBuilder message = new StringBuilder();
        message.append("Hi ");
        message.append(queueMessage.getUserMessage().getFirstName());
        message.append(", Your new account is created Successfully");
        String subject = "New Account Created";

        mailService.sendMail(
                message.toString(),
                subject,
                queueMessage.getUserMessage().getEmail(),
                FROM
        );
    }
}

Add Configuration for the queue

Create a UserMessageQueueConfig.java and add the required beans for the messaging queue.

configs/UserMessageQueueConfig.java

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class UserMessageQueueConfig
{

    public static final String USER_QUEUE_NAME="rbtmq_user_message_queue";

    public static final String EXCHANGE_NAME="rbtmq_user_message_exchange";

    public static final String ROUTING_KEY="rbtmq_user_routing_key";

    @Bean
    public Queue queue()
    {
        return new Queue(USER_QUEUE_NAME);
    }

    @Bean
    public TopicExchange topicExchange()
    {
        return new TopicExchange(EXCHANGE_NAME);
    }

    @Bean
    public Binding binding(Queue queue, TopicExchange topicExchange)
    {
        return BindingBuilder
                .bind(queue)
                .to(topicExchange)
                .with(ROUTING_KEY);
    }

    @Bean
    public MessageConverter messageConverter()
    {
        return new Jackson2JsonMessageConverter();
    }

    @Bean
    public AmqpTemplate template(ConnectionFactory connectionFactory)
    {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(messageConverter());
        return rabbitTemplate;
    }
}

Create a structure for the message

Create a UserMailQueueMessage.java and specify the structure of the message to be passed in the queue.

entities/UserMailQueueMessage.java

import lombok.*;

import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Builder
public class UserMailQueueMessage
{
    private String mailQueueMessageId;
    private User userMessage;
    private Date queueMessageDate;
}

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 start.spring.io

Add the following dependencies :

  • Spring Web

  • Lombok

  • Spring for RabbitMQ

For this article, we are using Spring Boot version 2.7.9 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, service URLs and RabbitMQ configs.

application.properties

server.port=8080

spring.rabbitmq.addresses = localhost:5672

url.user-service=http://localhost:8081/users
url.mail-service=http://localhost:8082/mail

Add Configuration for the queue

Create a UserMessageQueueConfig.java and add the required beans for the messaging queue.

configs/UserMessageQueueConfig.java

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class UserMessageQueueConfig
{

    public static final String USER_QUEUE_NAME="rbtmq_user_message_queue";

    public static final String EXCHANGE_NAME="rbtmq_user_message_exchange";

    public static final String ROUTING_KEY="rbtmq_user_routing_key";

    @Bean
    public Queue queue()
    {
        return new Queue(USER_QUEUE_NAME);
    }

    @Bean
    public TopicExchange topicExchange()
    {
        return new TopicExchange(EXCHANGE_NAME);
    }

    @Bean
    public Binding binding(Queue queue, TopicExchange topicExchange)
    {
        return BindingBuilder
                .bind(queue)
                .to(topicExchange)
                .with(ROUTING_KEY);
    }

    @Bean
    public MessageConverter messageConverter()
    {
        return new Jackson2JsonMessageConverter();
    }

    @Bean
    public AmqpTemplate template(ConnectionFactory connectionFactory)
    {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(messageConverter());
        return rabbitTemplate;
    }
}

Create a structure for the message

Create a UserMailQueueMessage.java and specify the structure of the message to be passed in the queue.

entities/UserMailQueueMessage.java

import lombok.*;

import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Builder
public class UserMailQueueMessage
{
    private String mailQueueMessageId;
    private User userMessage;
    private Date queueMessageDate;
}

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-amqp</artifactId>
        </dependency>
        <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>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-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.runnerserviceasyncrabbitmq.configs.UserMessageQueueConfig;
import com.umang345.runnerserviceasyncrabbitmq.entities.RunnerResponseDto;
import com.umang345.runnerserviceasyncrabbitmq.entities.User;
import com.umang345.runnerserviceasyncrabbitmq.entities.UserMailQueueMessage;
import com.umang345.runnerserviceasyncrabbitmq.entities.UserResponseDto;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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 org.springframework.web.client.RestTemplate;

import java.util.Date;
import java.util.UUID;

@RestController
@RequestMapping("/simulate/users")
public class RunnerController
{
    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @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 {

            response = restTemplate.exchange(userServiceUrl,HttpMethod.POST,new HttpEntity<>(newUser), UserResponseDto.class);

            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();


            UserMailQueueMessage userMailQueueMessage =
                    UserMailQueueMessage.builder()
                    .mailQueueMessageId(UUID.randomUUID().toString())
                    .queueMessageDate(new Date())
                    .userMessage(newUser)
                    .build();

            rabbitTemplate.convertAndSend(
                    UserMessageQueueConfig.EXCHANGE_NAME,
                    UserMessageQueueConfig.ROUTING_KEY,
                    userMailQueueMessage
            );

            return ResponseEntity.status(HttpStatus.OK).body(responseDto);
        }catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
        }
    }
}

With this, we complete our runner service.

Now, let's test our app

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

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