Send Email Notification using Springboot: Part 3 — Asynchronous approach using RabbitMQ
In Part 1, we explored sending emails synchronously, highlighting the significant delay it introduced, especially affecting the User registration API’s performance. Part 2 then optimized this API by making the sendEmail method asynchronous with the @Async annotation, resulting in a substantial reduction in delay. However, as we delved deeper, challenges arose when considering the scalability and management complexities of numerous threads in larger applications.
Challenges with @Async in Large Applications
- Thread Management Complexity: In larger applications, the default creation of a new thread for each asynchronous method invocation by
@Asyncintroduces complexity and becomes resource-intensive. - Limited Control over Concurrency: Spring’s default configuration for
@Asynclacks fine-grained control over thread pooling and concurrency limits, potentially leading to issues such as thread exhaustion. - Lack of Message Queue:
@Asynclacks a built-in message queue system, making it less suitable for scenarios where tasks need queuing, prioritization, or handling by multiple instances.
Advantages of JMS(Java Messaging Service) for Asynchronous Processing
- Robust Message Queues: JMS provides a robust and scalable message queuing system, facilitating decoupling between components and enabling asynchronous communication.
- Guaranteed Message Delivery: JMS brokers like RabbitMQ ensure reliable message delivery, queuing messages when a consumer is temporarily unavailable and delivering them upon accessibility.
- Scalability: JMS supports distributed processing, scaling well across multiple instances of an application, making it suitable for large-scale applications with high concurrency requirements.
- Fault Tolerance: JMS brokers often feature clustering and replication, providing fault tolerance. In case of a broker failure, another node can seamlessly take over, ensuring continuous message processing.
- Transaction Management: JMS supports transactional message processing, crucial for maintaining data consistency in applications with complex business logic.
While @Async suits simple asynchronous processing, its limitations become apparent in large-scale applications. For scenarios involving a significant number of asynchronous tasks, JMS, with its robust queuing mechanisms and scalability, emerges as a more suitable choice.
Configuring RabbitMQ for Email Sending
Add dependency in pom.xml.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>This dependency ensures that your Spring Boot application includes the necessary libraries for RabbitMQ integration.
Get Samyak Moon’s stories in your inbox
Join Medium for free to get updates from this writer.
YAML Configuration
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
rabbitmq:
queue:
email:
name: email_queue
exchange:
email:
name: email_exchange
binding:
email:
name: email_routing_keyThis YAML configuration ensures the integration of RabbitMQ for asynchronous email sending. It specifies the RabbitMQ server details, queue, exchange, and binding configurations for handling email-related messages.
Create a RabbitMQConfig class
@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
MessageConverter converter() {
return new Jackson2JsonMessageConverter();
}
}In the RabbitMQConfig class, we use Spring's @Configuration annotation to define beans for configuring RabbitMQ elements. These include the email queue, exchange, and binding. Additionally, an AmqpTemplate bean is created for sending messages to the RabbitMQ queue, and a MessageConverter bean is configured to convert messages to JSON format.
Implementing RabbitMQ in User Registration
RabbitMQ Listener
@RabbitListener(queues = "email_queue")
public void sendEmail(EmailDetails emailDetails){
try {
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setFrom(emailSender);
mailMessage.setTo(emailDetails.getRecipient());
mailMessage.setText(emailDetails.getMessageBody());
mailMessage.setSubject(emailDetails.getSubject());
javaMailSender.send(mailMessage);
log.info("Mail sent successfully");
}catch (MailException e){
log.debug("Sending mail failed due to some error");
}
}The @RabbitListener annotation marks the sendEmail method as a listener for the specified email queue (email_queue). This method processes incoming messages from the RabbitMQ queue, sending emails asynchronously.
User Registration with RabbitMQ
@Autowired
private UserRepository userRepository;
@Autowired
private RabbitTemplate rabbitTemplate;
@Value("${rabbitmq.exchange.email.name}")
private String emailExchange;
@Value("${rabbitmq.binding.email.name}")
private String emailRoutingKey;
@Override
public void registerUser(RequestDto requestDto) {
if(userRepository.existsByEmail(requestDto.getEmail()))
throw new UserAlreadyExistsException(UserConstants.USER_ALREADY_EXISTS);
User user = UserMapper.mapToUser(new User(),requestDto);
userRepository.save(user);
rabbitTemplate.convertAndSend(emailExchange,
emailRoutingKey,
EmailDetails.builder()
.messageBody("Registration Successful with mail id: "+requestDto.getEmail())
.recipient(requestDto.getEmail())
.subject("REGISTRATION SUCCESS")
.build());
}In the registerUser method, after validating and saving the user, the RabbitMQ convertAndSend method is employed to enqueue the email details for processing. This ensures that the email is sent asynchronously via RabbitMQ.
Result
Upon running the application and triggering the registration process, the asynchronous email processing via RabbitMQ significantly reduces the time to 189ms, compared to the 3.78s observed with the synchronous approach. This enhancement demonstrates the power of JMS, specifically RabbitMQ, in optimizing asynchronous processing within Spring Boot applications.
Conclusion
This multipart series navigated through different strategies for sending emails in a Spring Boot application. Part 1 uncovered the drawbacks of a synchronous email-sending approach, leading to a remedy in Part 2 by adopting an asynchronous approach using the @Async annotation. However, challenges emerged in larger applications. Part 3 leveraged the power of Java Message Service (JMS) with RabbitMQ, mitigating complexities introduced by managing numerous threads. This ensured improved scalability, fault tolerance, and transaction management, making it an ideal solution for large-scale applications.







