Implementing Asynchronous Retry Mechanism for Email Sending in Spring Applications

ALT TEXT

To enhance the performance and responsiveness of Spring applications, there is often a need to execute code asynchronously.

Additionally, handling occasional failures, such as network issues or third-party service unavailability, is crucial for building resilient applications.

In this article, we will explore the implementation of an asynchronous execution with an automatic retry mechanism for sending emails in a Spring application, leveraging Spring's support for async and retry operations.

Example Application in Spring Boot

Let's consider a scenario where we need to create a microservice that sends emails asynchronously, with the ability to automatically retry in case of transient failures.

Creating the Spring Boot Application

First, we need to create a new Spring Boot application using the Spring Initializr.

1. Adding Dependencies

Then, we’ll need to include the spring-boot-starter-web maven dependency:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Next we need the spring-boot-starter-mail maven dependency:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

Finally, we need to include the spring-retry dependency


<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

2. Configuring the Email Properties

Next, we need to configure the email properties in the application.yaml file:

spring:
  mail:
    username: ${MAIL_USERNAME}
    password: ${MAIL_PASSWORD}

Now, we need to create a configuration class to configure the JavaMailSender bean, which will be used to send emails.

@Configuration
public class AppConfiguration {

    @Value("${spring.mail.username}")
    String username;

    @Value("${spring.mail.password}")
    String password;

    @Bean
    public JavaMailSender javaMailSender() {

        JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
        mailSender.setHost("smtp.gmail.com");
        mailSender.setPort(587);

        mailSender.setUsername(username);
        mailSender.setPassword(password);

        Properties props = mailSender.getJavaMailProperties();
        props.put("mail.transport.protocol", "smtp");
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.starttls.enable", "true");

        return mailSender;
    }
}

3. Implementing the EmailService

Now, let's implement the EmailService class, which will be responsible for sending emails:

@Service
public class EmailService {

    private static final Logger logger = LoggerFactory.getLogger(EmailService.class);

    private final JavaMailSender javaMailSender;

    public EmailService(JavaMailSender javaMailSender) {
        this.javaMailSender = javaMailSender;
    }

    public String sendEmail(String to, String subject, String body) throws MessagingException, IOException {
        logger.info("Trying to send email to {}", to);

        String senderName = "Asynchronous Email Service";
        String from = "nidhalnaffati@gmail.com";

        MimeMessage message = javaMailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, "UTF-8");

        helper.setFrom(from, senderName);
        helper.setTo(to);
        helper.setSubject(subject);
        helper.setText(body);

        javaMailSender.send(message);
        logger.info("Email sent");

        return "Email sent to " + to;
    }
}

4. Implementing Asynchronous Execution With Retry

To achieve asynchronous execution with retry for the email sending process, follow these steps:

First, we need to add the @EnableAsync and @EnableRetry annotations to a configuration class to enable asynchronous execution and retry operations in the Spring Boot application.

We already have the AppConfiguration class, so we can add these annotations to it.

So it will look like this:

@EnableAsync
@EnableRetry
@Configuration
public class AppConfiguration {
    // same code as before
}

Next, we need to modify the EmailService class to execute the sendEmail method asynchronously and to retry the execution in case of a failure.

But before that I want to create a function to simulate the failure of the email sending process, so we can test the retry mechanism.

public void simulateRandomFailure() throws MessagingException {
        // generate a random number between 1 and 6
        int random = (int) (Math.random() * 6 + 1);

        logger.info("Random number: {}", random);

        if (random < 5) { // 4 out of 6 chances to fail
            logger.error("Simulating a random failure");
            throw new MessagingException("Failed to send email");
        }
}

Now we can modify the EmailService class to execute the sendEmail method asynchronously and to retry the execution in case of MessagingException, with a maximum of 4 attempts and a delay of 3 seconds between each attempt.

@Service
public class EmailService {

    private static final Logger logger = LoggerFactory.getLogger(EmailService.class);

    private final JavaMailSender javaMailSender;

    public EmailService(JavaMailSender javaMailSender) {
        this.javaMailSender = javaMailSender;
    }

    @Retryable(
        retryFor = MessagingException.class, // specify the exception to retry
        maxAttempts = 4, // default is 3
        backoff = @Backoff(delay = 3000) // set the backoff delay to 3 seconds
    )
    public String sendEmailWithRetry(String to, String subject, String body) throws MessagingException, IOException {
        simulateRandomFailure();

        return sendEmail(to, subject, body);
    }
    
    @Recover
    public String handleMessagingException(MessagingException e) {
        logger.error("Max attempts reached. Failed to send email after 4 attempts.");
        logger.error("Error message: {}", e.getMessage());

        return "Max attempts reached. Failed to send email";
    }
    
    public String sendEmail(String to, String subject, String body) throws MessagingException, IOException {
        // same code as before
    }

5. Creating a REST Controller

Finally, to test the retry mechanism, we can create a REST controller to expose an endpoint to send emails:

@RestController
public class EmailController {
    private final EmailService emailService;

    public EmailController(EmailService emailService) {
        this.emailService = emailService;
    }

    static String to = "nidhalnaffati@gmail.com";
    static String subject = "Asynchronous Email";
    static String body = "Hello, this is an asynchronous email";


    @GetMapping("/send-email")
    public String sendEmail() throws MessagingException, IOException {
        return emailService.sendEmailWithRetry(to, subject, body);
    }
}

Now, we can run the application and send an email using the /send-email endpoint.

As you can see in the following images the email sending process failed at the first attempt, then it was retried one more time and it succeeded.

screenshot-1

I have executed the /send-email endpoint multiple times till I got the following result:

screenshot-2

This time after the 4 attempts, the email sending process failed and the recover method was called.

Conclusion

In this article, we explored the implementation of an asynchronous execution with an automatic retry mechanism for sending emails in a Spring application, leveraging Spring's support for async and retry operations.

This approach can be used to enhance the performance and responsiveness of Spring applications, and to handle occasional failures, such as network issues or third-party service unavailability, to build resilient applications.