Sending Email Notifications with RabbitMQ Using Java (Spring Boot)

Olulode Olatunbosun
7 min readMay 19, 2024

--

RabbitMQ serves as a message broker, acting as a middleman to facilitate communication between different parts of a software system. It manages message queuing, routing, and delivery, enabling components to interact seamlessly. In this tutorial, you’ll learn how to set up RabbitMQ using Docker, connect it with a Spring Boot application, and use it to send email notifications asynchronously.

In this tutorial, you will learn the following

  • What is async programming?
  • How to Setting up rabbitmq using docker
  • Connect it with Spring boot application
  • Send email notification asynchronously

What is Asynchronous Programming?

Asynchronous programming enables tasks to operate independently of the main program flow, allowing multiple operations to execute concurrently. Unlike synchronous programming, where tasks are executed sequentially, asynchronous programming allows the program to initiate a task and proceed with other tasks while awaiting the completion of the initial one. This method prevents blocking, enhances performance, and optimizes resource usage. Asynchronous programming is particularly useful when dealing with tasks such as sending emails, generating reports with large volumes of data, verifying transactions, and more.

Setting up a spring boot application

To initiate the setup of a Spring Boot application, I typically utilize Spring Initializr to kickstart the project. Below, I’ll outline the process of adding necessary dependencies for our application. In this particular use case, we’ll include Spring for RabbitMQ, as it’s integral to the purpose of this application. Additionally, you’ll find a list of other dependencies that are essential for configuring the Spring Boot application. Below snapshot covers the setup we need to get the application.

snapshot of setting up springboot application

Once the application setup is complete, you’ll generate and open it in your Integrated Development Environment (IDE), where you should see a structure resembling the image below:

a screenshot of the structure of the application

Following the setup, I’ll proceed to create the required classes and structure for our application. The resulting structure may resemble the image below, but it’s not mandatory to strictly adhere to this convention.

this screenshot shows how i structured my project

Setting Up RabbitMQ Using Docker with docker compose

Create a docker-compose.yml file in the root directory of your Spring Boot application to set up RabbitMQ:

version: '3.8'  # specify your Docker Compose version
services:
rabbitmq:
container_name: rabbitmq-new
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USERNAME} # RabbitMQ username
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} # RabbitMQ password

This configuration exposes both the RabbitMQ server port (5672) and the web console port (15672). The management tag in the Docker image enables access to the web interface. Set up the default user and password using environment variables.

You will also need to create a bash script start-service.sh to start the Docker container with a bash script

#!/bin/bash
export RABBITMQ_USERNAME=myuser
export RABBITMQ_PASSWORD=mypassword

docker compose up

Once we’ve created the bash script to start our application, we need to make it executable by running the command chmod +x nameofyourscript.sh. To run the script, we use ./nameofyourscript.sh.

a snapshot showing how i started the docker file

Upon execution, we'll observe RabbitMQ starting up within Docker. We can access its web interface through the exposed port specified in the Docker Compose file. This interface provides insights and control over RabbitMQ's functionalities.

a snapshot for the login interface

After logging into the interface using the credentials set in the bash script, we’ll access the following interface.

snapshot of the rabbitmq interface where we see our queues

Connecting RabbitMQ with Spring Boot for Email Notifications

RabbitMQ Configuration Class

Create a configuration class named RabbitMQConfig in your Spring Boot application. Add the following three properties to the application.yml file, each serving a distinct purpose:

  1. Queue name: Specifies the name of the queue where messages will be stored before processing.
  2. Exchange email name: Identifies the exchange to which messages will be sent for email processing.
  3. Email routing key: Determines the routing of messages within the exchange for email-related operations.
@Configuration
public class RabbitMQConfig {
@Value("${rabbitmq.queue.email.name}")
private String emailQueue;

@Value("${rabbitmq.exchange.email.name}")
private String emailExchange;

@Value("${rabbitmq.binding.email.name}")
private String emailRoutingKey;

@Bean
public Queue emailQueue() {
return new Queue(emailQueue);
}

@Bean
public TopicExchange emailExchange() {
return new TopicExchange(emailExchange);
}

@Bean
public Binding emailBinding() {
return BindingBuilder.bind(emailQueue())
.to(emailExchange())
.with(emailRoutingKey);
}

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

@Bean
public Jackson2JsonMessageConverter converter() {
return new Jackson2JsonMessageConverter();
}
}

This class sets up the RabbitMQ queue, exchange, and binding. It also configures an AmqpTemplate for sending messages and a message converter for JSON format

Email Detail DTO

What is DTO( Data transfer object) ? A Data Transfer Object (DTO) is a design pattern used for transferring data between software components or layers within an application. In the context this email detail DTO, it’s a structure that holds essential information about an email, such as sender, recipients, subject, and body. This DTO allows for organized and efficient transfer of email-related data between different parts of the application.

Define a Data Transfer Object (DTO) for email details:

@Data
@Builder
public class EmailDetailDTO {
private String to;
private String subject;
private Map<String, Object> dynamicValue;
private String templateName;
}

Email Service Class

Create a service class EmailService to handle email sending:

@Service
@RequiredArgsConstructor
public class EmailService {
private final JavaMailSender emailSender;

@Value("${application.mail.sent.from}")
private String fromUsr;

public void sendEmail(String to, String subject, String body) throws MessagingException {
MimeMessage message = emailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setTo(to);
helper.setFrom(fromUsr);
helper.setSubject(subject);
helper.setText(body, true);
emailSender.send(message);
}

@RabbitListener(queues = "email_queue")
public void processEmailMessage(EmailDetailDTO emailDetailDTO) throws MessagingException {
String to = emailDetailDTO.getTo();
String subject = emailDetailDTO.getSubject();
String body = generateEmailBody(emailDetailDTO);
sendEmail(to, subject, body);
}

public String generateEmailBody(EmailDetailDTO emailDetailDTO) {
String templateName = emailDetailDTO.getTemplateName();
String template = loadEmailTemplate(templateName);

String body = template;
if (emailDetailDTO.getDynamicValue() != null) {
for (Map.Entry<String, Object> entry : emailDetailDTO.getDynamicValue().entrySet()) {
body = body.replace("{{" + entry.getKey() + "}}", entry.getValue().toString());
}
}

return body;
}

public String loadEmailTemplate(String templateName) {
ClassPathResource resource = new ClassPathResource("templates/emails/" + templateName + ".html");
try {
return StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException("Error loading email template " + templateName, e);
}
}
}

This service listens for messages on the email_queue, processes the email details, and sends the email using JavaMailSender.

User Service Implementation

Implement the user service to handle user registration and send a verification email:

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final RabbitTemplate rabbitTemplate;

@Value("${rabbitmq.exchange.email.name}")
private String emailExchange;

@Value("${rabbitmq.binding.email.name}")
private String emailRoutingKey;

@Override
@Transactional
public GenericResponseDTO saveUser(CreateUserDTO createUserDTO) {
String verificationToken = TokenGenerator.generateToken(6);
User user = modelMapper.map(createUserDTO, User.class);
GenericResponseDTO responseDTO = new GenericResponseDTO();
user.setVerificationCode(verificationToken);
user.setPassword(passwordEncoder.encode(createUserDTO.getPassword()));

try {
User savedUser = userRepository.save(user);
sendEmail(user, verificationToken, "Email Verification");
responseDTO.setMessage("User created successfully");
responseDTO.setStatus("success");
return responseDTO;
} catch (Exception e) {
responseDTO.setMessage("An error occurred: " + e.getMessage());
responseDTO.setStatus("error");
responseDTO.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
return responseDTO;
}
}

private void sendEmail(User user, String token, String subject) {
Map<String, Object> mailData = Map.of("token", token, "fullName", user.getFullName());

rabbitTemplate.convertAndSend(emailExchange, emailRoutingKey, EmailDetailDTO.builder()
.to(user.getEmail())
.subject(subject)
.dynamicValue(mailData)
.templateName("verification")
.build());
}
}

The EmailService class facilitates email sending within the application by utilising JavaMailSender for construction and delivery of emails, while also listening for messages on the “email_queue” RabbitMQ queue, processing email details, generating email bodies from templates, and sending emails accordingly.

The UserController implementation

Here’s the implementation of the UserController, providing a clearer explanation of how it handles user-related operations within the application.

@RestController
@RequiredArgsConstructor
@RequestMapping("users/")
public class UserController {

private final UserService userService;
@PostMapping("/register")
public ResponseEntity<GenericResponseDTO> registerUser(@RequestBody CreateUserDTO createUserDTO){
GenericResponseDTO response = userService.saveUser(createUserDTO);
if (response.getStatus().equals("error")){
return new ResponseEntity<>(response, HttpStatusCode.valueOf(response.getStatusCode()));
}
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
}

The UserController class acts as a REST controller, managing user registration via a POST endpoint "/users/register", utilizing UserService to process user data, and returning appropriate responses, including status and HTTP status codes.

We’re going to conduct a test on the application to observe the email being sent and the queue being processed. We’ll review screenshots to confirm the successful execution of these actions.

a snapshot if when the queue has not processed any message

In the above provided snapshot, it’s evident that the “email_queue” has not processed any messages, as indicated by both the “unacked” and “total” counts being 0. This suggests that there are currently no messages awaiting processing within the queue.

Take a look at this screenshot. It’s clear that the “email_queue” has processed a message, as indicated by both the “unacked” and “total” counts being 1. This indicates that there are messages currently awaiting processing within the queue.

Conclusion

In this article, you learned how to set up and use RabbitMQ with Spring Boot to send email notifications asynchronously. We covered the following key points:

  1. Understanding Asynchronous Programming: The basics of asynchronous programming and its benefits.
  2. Setting Up RabbitMQ Using Docker: How to configure and run RabbitMQ using Docker.
  3. Integrating RabbitMQ with Spring Boot: Creating configuration and service classes to enable RabbitMQ messaging.
  4. Sending Email Notifications: Implementing a service to send emails using JavaMailSender and processing messages from RabbitMQ.

By following this tutorial, you now have a working setup for handling email notifications in an efficient and scalable way, leveraging the power of RabbitMQ for message queuing.

You can find the full code for this here on github

Keep learning, and happy coding!

Let connect on LinkedIn and Twitter.

--

--