How to Implement Styled Email Sending Using Amazon SES and Spring Boot

Eduardo Vinicius Frohlich
7 min readMay 7, 2024

--

In today’s digital world, where communication is vital, email remains one of the most effective ways to reach users and customers. Whether it’s for registration confirmation, event notifications, or personalized marketing messages, emails are a direct bridge to the audience. In this article, we will explore how to integrate Amazon Simple Email Service (SES), a scalable and cost-effective email sending service, with Spring Boot, one of the most popular frameworks for creating Java applications. The following project was based on a challenge resolution for a position at Uber made by Fernanda Kipper | Dev available at https://www.youtube.com/watch?v=eFgeO9M9lLw, which inspired me to build my application.

Available on my GitHub

🇧🇷 Portuguese version:

Amazon Simple Email Service (SES)

Technologies Used

  • Java 17
  • Spring Boot 3.2.5
  • Postman
  • Thymeleaf: A template engine for Java, used to process HTML on the server-side.
  • BEE Free: An online tool (https://beefree.io) that allows us to create responsive and visually appealing email designs without needing deep knowledge in HTML and CSS.
  • AWS Java SDK: The Amazon Java Software Development Kit allows any Java application to be easily integrated with AWS services, including Amazon Simple Email Service (SES).

Create your email address on Amazon SES

It is through this email that you will send emails to your recipients.

You must have an account already registered on the AWS console. Check your email for AWS confirmation to use the resource.

Project Structure in the IDE

pom.xml (adapt for your versions or packages)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.eduardo.frohlich</groupId>
<artifactId>aws-ses</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>aws-ses</name>
<description>AWS Simple Email Service application</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
<!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk</artifactId>
<version>1.12.712</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</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>
</project>

application.properties (insert your verified email credentials within AWS SES).

aws.region=<YOUR-REGION>
aws.accessKeyId=<YOUR-ACCESS-KEY>
aws.secretKey=<YOUR-SECRET-KEY>

Copying the HTML from our email template by beefree

Click on copy the html and copy the content into an HTML file inside the src/main/resources/templates folder

Configuring variables inside the HTML with Thymeleaf

Use [[${var}]] to indicate that the HTML text is a variable. For more information about Thymeleaf notations, check here.

account-password.html (template HTML generated by Beefree)

Available on github

Building our SpringBoot application

Using the basis of a clean architecture, we create the following packages with their contents:

Core layer: contains the entities, central business rules, and use cases (defines what our app does).

  • EmailRequest — DTO for email shots without HTML.
package com.eduardo.frohlich.emailservice.core;

public record EmailRequest(String to, String subject, String body) {
}
  • HtmlEmailRequest — DTO for email shots with HTML template.
package com.eduardo.frohlich.emailservice.core;

public record HtmlEmailRequest(String subject, String to, String name, String templateName) {
public HtmlEmailRequest {
templateName = "account-password";
}
}
  • EmailSenderUseCase — use case of the entities that will be implemented within the application layer
package com.eduardo.frohlich.emailservice.core;

public interface EmailSenderUseCase {
void sendEmail(String to, String subject, String body);
void sendHtmlEmail(String to, String subject, String body);
}

Configuring Exceptions

Create the exceptions package within core

package com.eduardo.frohlich.emailservice.core.exceptions;

public class EmailServiceException extends RuntimeException {
public EmailServiceException(String message, Throwable cause) {
super(message, cause);
}
}

Application layer: where we will implement the core of our application.

  • EmailSenderService — service where we implement our use case interface and call our adapter interface.
package com.eduardo.frohlich.emailservice.application;

import com.eduardo.frohlich.emailservice.adapters.EmailSenderGateway;
import com.eduardo.frohlich.emailservice.core.EmailSenderUseCase;
import com.eduardo.frohlich.emailservice.core.HtmlEmailRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;
import java.util.Map;
@Service
public class EmailSenderService implements EmailSenderUseCase {
private static final Logger LOGGER = LoggerFactory.getLogger(EmailSenderService.class);
@Autowired
private SpringTemplateEngine templateEngine;
private final EmailSenderGateway emailSenderGateway;
@Autowired
public EmailSenderService(EmailSenderGateway emailSenderGateway) {
this.emailSenderGateway = emailSenderGateway;
}
@Override
public void sendEmail(String to, String subject, String body) {
emailSenderGateway.sendEmail(to, subject, body);
}
@Override
public void sendHtmlEmail(String to, String subject, String body) {
emailSenderGateway.sendHtmlEmail(to, subject, body);
}
@Async
public void htmlSend(HtmlEmailRequest HTMLrequest, Map<String, Object> model) {
Context context = new Context();
context.setVariables(model);
String templateName = HTMLrequest.templateName();
String to = HTMLrequest.to();
String subject = HTMLrequest.subject();
String processedHtml = templateEngine.process(templateName, context);
LOGGER.debug("Processed HTML: {}", processedHtml);
sendHtmlEmail(to, subject, processedHtml);
}
}

Adapters layer: where we will make direct contact with the AWS service provider through an interface.

  • EmailSenderGateway — we will use this interface to implement it to the AWS SES email sending service.
package com.eduardo.frohlich.emailservice.adapters;

public interface EmailSenderGateway {
void sendEmail(String to, String subject, String body);
void sendHtmlEmail(String to, String subject, String body);
}

Infrastructure Layer: Contains the implementation of the interfaces defined in the adapters for the AWS SES service.

Packages are separated in case you need to create another email sending API. The ‘ses’ package is created within infra.

  • AwsSesConfig — AmazonSimpleEmailService configuration. We declare our AWS credentials which are defined in the application.properties (you may use another method to securely store your credentials).
package com.eduardo.frohlich.emailservice.infra.ses;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.simpleemail.AmazonSimpleEmailService;
import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AwsSesConfig {
@Value("${aws.region}")
private String awsRegion;
@Value("${aws.accessKeyId}")
private String accessKey;
@Value("${aws.secretKey}")
private String secretKey;
@Bean
public AmazonSimpleEmailService amazonSimpleEmailService() {
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
return AmazonSimpleEmailServiceClientBuilder
.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(awsRegion)
.build();
}
}
  • SesEmailSender — Implements our adapter interface EmailSenderGateway to send emails to users through our configured account for email dispatches. We call the methods of sendEmail and sendHtmlEmail.
package com.eduardo.frohlich.emailservice.infra.ses;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.simpleemail.AmazonSimpleEmailService;
import com.amazonaws.services.simpleemail.model.*;
import com.eduardo.frohlich.emailservice.adapters.EmailSenderGateway;
import com.eduardo.frohlich.emailservice.core.exceptions.EmailServiceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class SesEmailSender implements EmailSenderGateway {
private static final Logger LOGGER = LoggerFactory.getLogger(SesEmailSender.class);
private final AmazonSimpleEmailService amazonSimpleEmailService;
@Autowired
public SesEmailSender(AmazonSimpleEmailService amazonSimpleEmailService) {
this.amazonSimpleEmailService = amazonSimpleEmailService;
}
@Override
public void sendEmail(String toEmail, String subject, String body) {
SendEmailRequest request = new SendEmailRequest()
.withSource("<YOUR-AWS-MAIL>.com")
.withDestination(new Destination().withToAddresses(toEmail))
.withMessage(new Message()
.withSubject(new Content(subject))
.withBody(new Body().withText(new Content(body)))
);
try {
amazonSimpleEmailService.sendEmail(request);
} catch (AmazonServiceException ex) {
throw new EmailServiceException("Email sending failed", ex);
}
}
@Override
public void sendHtmlEmail(String to, String subject, String body) {
SendEmailRequest request = new SendEmailRequest()
.withSource("<YOUR-AWS-MAIL>.com")
.withDestination(new Destination().withToAddresses(to))
.withMessage(new Message()
.withSubject(new Content(subject))
.withBody(new Body().withHtml(new Content(body)))
);
try {
amazonSimpleEmailService.sendEmail(request);
LOGGER.info("Email sent successfully to {}", to);
} catch (AmazonServiceException ex) {
LOGGER.error("Error sending HTML email", ex);
throw new EmailServiceException("Email sending failed", ex);
}
}
}

Config Layer: Configuration of Thymeleaf for rendering email templates.

  • ThymeleafTemplateConfig — We configure the folder where our HTML email template is located so that Thymeleaf can generate the page (by convention, HTML files are inside the ‘templates’ folder within the project’s resources).
package com.eduardo.frohlich.emailservice.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import java.nio.charset.StandardCharsets;
@Configuration
public class ThymeleafTemplateConfig {
@Bean
public SpringTemplateEngine springTemplateEngine() {
SpringTemplateEngine springTemplateEngine = new SpringTemplateEngine();
springTemplateEngine.addTemplateResolver(emailTemplateResolver());
return springTemplateEngine;
}
public ClassLoaderTemplateResolver emailTemplateResolver() {
ClassLoaderTemplateResolver emailTemplateResolver = new ClassLoaderTemplateResolver();
emailTemplateResolver.setPrefix("/templates/");
emailTemplateResolver.setSuffix(".html");
emailTemplateResolver.setTemplateMode(TemplateMode.HTML);
emailTemplateResolver.setCharacterEncoding(StandardCharsets.UTF_8.name());
emailTemplateResolver.setCacheable(false);
return emailTemplateResolver;
}
}

Controllers Layer: Controllers that manage HTTP requests.

  • EmailSenderController — HTTP requests for sending standard or HTML-stylized email.
package com.eduardo.frohlich.emailservice.controllers;

import com.eduardo.frohlich.emailservice.application.EmailSenderService;
import com.eduardo.frohlich.emailservice.core.EmailRequest;
import com.eduardo.frohlich.emailservice.core.HtmlEmailRequest;
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;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/email")
public class EmailSenderController {
private final EmailSenderService emailSenderService;
@Autowired
public EmailSenderController(EmailSenderService emailSenderService) {
this.emailSenderService = emailSenderService;
}
@PostMapping
public ResponseEntity<String> sendEmail(@RequestBody EmailRequest emailRequest) {
try {
emailSenderService.sendEmail(emailRequest.to(), emailRequest.subject(), emailRequest.body());
return ResponseEntity.ok("Email sent successfully");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error sending email");
}
}
@PostMapping("/sendHtmlEmail")
public ResponseEntity<String> sendHtmlEmail(@RequestBody HtmlEmailRequest htmlEmailRequest) {
try {
Map<String, Object> model = new HashMap<>();
model.put("name", htmlEmailRequest.name());
model.put("password", "123456");
emailSenderService.htmlSend(htmlEmailRequest, model);
return ResponseEntity.ok("Email sent successfully");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error sending email: " + e.getMessage());
}
}
}

Testing with Postman

{
"to": "<YOUR-AWS-MAIL>@gmail.com",
"body": "Your password is 12345",
"subject": "Hello! Welcome to Bantads"
}
  • Email result

sendHtmlEmail: Testing email to http://localhost:8080/api/email/sendHtmlEmail. We configure in the controller method sendHtmlEmail to pass the parameter "name" that we defined in the variable name inside the account-password.html.

{
"to": "<YOUR-AWS-MAIL>@gmail.com",
"subject": "Hello! Copy your password",
"name": "Username"
}
  • Email result

Thank you very much for taking the time to read this article. I hope the information shared will be useful in your future projects. I appreciate any feedback and am available to discuss more on the topic. See you next time!

--

--

Eduardo Vinicius Frohlich

Enthusiastic and detail-oriented Systems Analyst in training, specializing in backend development with echnologies including Spring Boot, JSF, REST APIs, etc.