Back to Blog

How to Send Emails in Java / Spring Boot (2026 Guide)

14 min read

Java has JavaMail for SMTP-based email sending. Spring Boot wraps it with spring-boot-starter-mail. Both work, but they leave you managing SMTP servers, TLS, and deliverability.

For production apps, API-based providers handle all of that. This guide covers Spring Mail for SMTP, REST API calls for modern providers, Thymeleaf templates, and async sending. All examples use Spring Boot 3+ with Java 17+.

Spring Mail vs API Providers

// Spring Mail: SMTP-based, you manage the server
@Autowired JavaMailSender mailSender;
SimpleMailMessage msg = new SimpleMailMessage();
msg.setTo("user@example.com");
msg.setSubject("Hello");
msg.setText("Body");
mailSender.send(msg);
 
// API provider: one HTTP call, they handle everything
restTemplate.postForObject(
    "https://api.sequenzy.com/v1/transactional/send",
    new EmailRequest("user@example.com", "Hello", "<p>Body</p>"),
    Map.class
);

Use Spring Mail if you're talking to an internal SMTP server. Use an API provider for everything else.

Pick a Provider

  • Sequenzy is built for SaaS. Transactional emails, marketing campaigns, automated sequences from one API. Native Stripe integration.
  • Resend is developer-friendly with a clean API. They have one-off broadcast campaigns but no automations or sequences.
  • SendGrid is the enterprise option. Has an official Java SDK. Good for high volume.

Add Dependencies

pom.xml
<!-- Spring WebClient for HTTP calls -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
pom.xml
<!-- Resend Java SDK -->
<dependency>
  <groupId>com.resend</groupId>
  <artifactId>resend-java</artifactId>
  <version>3.1.0</version>
</dependency>
pom.xml
<!-- SendGrid Java SDK -->
<dependency>
  <groupId>com.sendgrid</groupId>
  <artifactId>sendgrid-java</artifactId>
  <version>4.10.2</version>
</dependency>

Add your API key to application.properties:

application.properties
sequenzy.api-key=${SEQUENZY_API_KEY}
application.properties
resend.api-key=${RESEND_API_KEY}
application.properties
sendgrid.api-key=${SENDGRID_API_KEY}

Create an Email Service

src/main/java/com/example/service/EmailService.java
package com.example.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.Map;

@Service
public class EmailService {

  private final WebClient webClient;

  public EmailService(@Value("${sequenzy.api-key}") String apiKey) {
      this.webClient = WebClient.builder()
          .baseUrl("https://api.sequenzy.com/v1")
          .defaultHeader("Authorization", "Bearer " + apiKey)
          .defaultHeader("Content-Type", "application/json")
          .build();
  }

  public Map<String, Object> send(String to, String subject, String body) {
      return webClient.post()
          .uri("/transactional/send")
          .bodyValue(Map.of("to", to, "subject", subject, "body", body))
          .retrieve()
          .bodyToMono(Map.class)
          .block();
  }
}
src/main/java/com/example/service/EmailService.java
package com.example.service;

import com.resend.Resend;
import com.resend.core.exception.ResendException;
import com.resend.services.emails.model.CreateEmailOptions;
import com.resend.services.emails.model.CreateEmailResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class EmailService {

  private final Resend resend;
  private static final String FROM = "Your App <noreply@yourdomain.com>";

  public EmailService(@Value("${resend.api-key}") String apiKey) {
      this.resend = new Resend(apiKey);
  }

  public CreateEmailResponse send(String to, String subject, String html)
          throws ResendException {
      CreateEmailOptions params = CreateEmailOptions.builder()
          .from(FROM)
          .to(to)
          .subject(subject)
          .html(html)
          .build();

      return resend.emails().send(params);
  }
}
src/main/java/com/example/service/EmailService.java
package com.example.service;

import com.sendgrid.*;
import com.sendgrid.helpers.mail.Mail;
import com.sendgrid.helpers.mail.objects.Content;
import com.sendgrid.helpers.mail.objects.Email;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;

@Service
public class EmailService {

  private final SendGrid sg;
  private static final String FROM = "noreply@yourdomain.com";

  public EmailService(@Value("${sendgrid.api-key}") String apiKey) {
      this.sg = new SendGrid(apiKey);
  }

  public Response send(String to, String subject, String html) throws IOException {
      Email from = new Email(FROM);
      Email toEmail = new Email(to);
      Content content = new Content("text/html", html);
      Mail mail = new Mail(from, subject, toEmail, content);

      Request request = new Request();
      request.setMethod(Method.POST);
      request.setEndpoint("mail/send");
      request.setBody(mail.build());

      return sg.api(request);
  }
}

REST Controller

src/main/java/com/example/controller/EmailController.java
package com.example.controller;

import com.example.service.EmailService;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/email")
public class EmailController {

  private final EmailService emailService;

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

  record WelcomeRequest(String email, String name) {}

  @PostMapping("/send-welcome")
  public Map<String, Object> sendWelcome(@RequestBody WelcomeRequest request) {
      return emailService.send(
          request.email(),
          "Welcome, " + request.name(),
          "<h1>Welcome, " + request.name() + "</h1><p>Your account is ready.</p>"
      );
  }
}
src/main/java/com/example/controller/EmailController.java
package com.example.controller;

import com.example.service.EmailService;
import com.resend.core.exception.ResendException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/email")
public class EmailController {

  private final EmailService emailService;

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

  record WelcomeRequest(String email, String name) {}

  @PostMapping("/send-welcome")
  public ResponseEntity<Map<String, String>> sendWelcome(@RequestBody WelcomeRequest request)
          throws ResendException {
      var result = emailService.send(
          request.email(),
          "Welcome, " + request.name(),
          "<h1>Welcome, " + request.name() + "</h1><p>Your account is ready.</p>"
      );
      return ResponseEntity.ok(Map.of("id", result.getId()));
  }
}
src/main/java/com/example/controller/EmailController.java
package com.example.controller;

import com.example.service.EmailService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.Map;

@RestController
@RequestMapping("/api/email")
public class EmailController {

  private final EmailService emailService;

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

  record WelcomeRequest(String email, String name) {}

  @PostMapping("/send-welcome")
  public ResponseEntity<Map<String, Boolean>> sendWelcome(@RequestBody WelcomeRequest request)
          throws IOException {
      emailService.send(
          request.email(),
          "Welcome, " + request.name(),
          "<h1>Welcome, " + request.name() + "</h1><p>Your account is ready.</p>"
      );
      return ResponseEntity.ok(Map.of("sent", true));
  }
}

Thymeleaf Email Templates

Spring Boot's Thymeleaf integration works for email templates too.

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- src/main/resources/templates/emails/welcome.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <body style="font-family: sans-serif; background: #f6f9fc; padding: 40px 0;">
    <div style="max-width: 480px; margin: 0 auto; background: #fff; padding: 40px; border-radius: 8px;">
      <h1 style="font-size: 24px;" th:text="'Welcome, ' + ${name}">Welcome</h1>
      <p style="font-size: 16px; line-height: 1.6; color: #374151;">
        Your account is ready. Click below to get started.
      </p>
      <a th:href="${loginUrl}"
         style="display:inline-block; background:#f97316; color:#fff; padding:12px 24px; border-radius:6px; text-decoration:none;">
        Go to Dashboard
      </a>
    </div>
  </body>
</html>
// Render Thymeleaf template to HTML string
@Service
public class EmailTemplateService {
    private final SpringTemplateEngine templateEngine;
 
    public EmailTemplateService(SpringTemplateEngine templateEngine) {
        this.templateEngine = templateEngine;
    }
 
    public String renderWelcome(String name, String loginUrl) {
        Context context = new Context();
        context.setVariable("name", name);
        context.setVariable("loginUrl", loginUrl);
        return templateEngine.process("emails/welcome", context);
    }
}

Async Email Sending

Don't block your request thread. Use @Async for fire-and-forget email sending.

@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean
    public Executor emailExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(5);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("email-");
        executor.initialize();
        return executor;
    }
}
@Service
public class AsyncEmailService {
    private final EmailService emailService;
 
    public AsyncEmailService(EmailService emailService) {
        this.emailService = emailService;
    }
 
    @Async("emailExecutor")
    public void sendAsync(String to, String subject, String body) {
        try {
            emailService.send(to, subject, body);
        } catch (Exception e) {
            log.error("Failed to send email to {}: {}", to, e.getMessage());
        }
    }
}

Going to Production

1. Verify Your Domain

Add SPF, DKIM, DMARC records. Required for deliverability.

2. Use Spring Profiles for Config

# application-prod.properties
sequenzy.api-key=${SEQUENZY_API_KEY}

3. Add Retry with Spring Retry

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2))
public Map<String, Object> send(String to, String subject, String body) {
    // ... existing send logic
}

Beyond Transactional

Sequenzy handles transactional sends, marketing campaigns, automated sequences, and subscriber management from one API. Native Stripe integration for SaaS.

Wrapping Up

  1. Spring Mail vs API providers and when to use each
  2. REST controllers for email endpoints
  3. Thymeleaf templates for maintainable HTML
  4. @Async for non-blocking sends
  5. Spring Retry for production reliability

Pick your provider, add the dependency, and start sending.