Envello
Tutorial

Sending transactional email from Spring Boot with Envello

Envello Team·2026-07-29·8 min read

Spring Boot's default email support goes through JavaMail and an SMTP transport. It works, but gives you none of the delivery visibility a modern email API provides: no webhook events, no structured bounce handling, no per-message delivery status you can query later. For transactional email in production, an HTTP API call is simpler and more observable.

This guide shows how to send transactional email from a Spring Boot application using Envello's REST API. No extra dependencies beyond what Spring Boot already includes.

Prerequisites

You need a Spring Boot 3.x project with spring-boot-starter-web on the classpath (which brings in RestClient), an Envello account with a verified sending domain, and your API key stored in application.yml or an environment variable.

Configuration

Add your API key to application.yml. Never hardcode credentials in source files.

  • envello.api-key: ${ENVELLO_API_KEY}
  • envello.base-url: https://api.envello.dev
  • envello.from: YourApp <[email protected]>

Create the email service

Spring Boot 3.1+ includes RestClient, a synchronous HTTP client that replaces the older RestTemplate. Use it to call Envello's API directly. Create a service class that encapsulates the send logic.

  • @Service class with @Value-injected config: apiKey, baseUrl, defaultFrom
  • A SendEmailRequest record with fields: from, to, subject, html, text (optional), cc, bcc
  • A sendEmail method that builds the HTTP POST request with Authorization: Bearer header and JSON body
  • RestClient.create(baseUrl).post().uri("/emails").header("Authorization", "Bearer " + apiKey).contentType(MediaType.APPLICATION_JSON).body(request).retrieve().toBodilessEntity()

Error handling

The API returns standard HTTP status codes. Handle them explicitly rather than letting Spring's default error handler swallow the details.

A 2xx response means the message was accepted for delivery (not yet delivered). A 400 means validation failed: bad address, unverified domain, or a suppressed recipient. A 401/403 means an invalid or expired API key. A 429 means you've hit the rate limit and should retry with backoff. A 5xx means a server-side error.

  • Wrap the RestClient call in a try-catch for RestClientResponseException
  • Log the status code and response body on failure, not just the exception message
  • For 429 responses, implement exponential backoff with a configurable retry count
  • For 5xx responses, retry once with a short delay before giving up
  • For 400 responses, don't retry; the request is invalid and needs to be fixed

Async sending with @Async

For transactional emails triggered by a web request (password reset, signup confirmation), the API call shouldn't block the HTTP response to your user. Spring's @Async annotation runs the send in a background thread.

  • Enable async processing with @EnableAsync on your configuration class
  • Mark the sendEmail method (or a wrapper) with @Async
  • Configure a TaskExecutor bean with a bounded thread pool (e.g., 5-10 threads for email sending)
  • Return CompletableFuture<Void> from the async method so callers can handle results if needed
  • Be aware that @Async exceptions don't propagate to the caller by default; configure an AsyncUncaughtExceptionHandler to log failures

Using Spring's TaskExecutor for higher volume

For applications sending hundreds or thousands of transactional emails per hour, a dedicated TaskExecutor with a bounded queue prevents thread pool exhaustion and provides backpressure.

Configure a ThreadPoolTaskExecutor with corePoolSize matching your expected concurrency (5-10 for most apps), maxPoolSize for spikes, and a queueCapacity that buffers during bursts without consuming unbounded memory. If the queue fills, the rejection policy should log and retry rather than silently dropping the send.

Structured logging

Log every send attempt with enough context to debug delivery issues without exposing PII.

  • Log: message ID (returned by the API), recipient domain (not the full address), subject line hash or first 20 characters, status code
  • Do not log: full recipient email address, email body content, API key
  • Use structured logging (SLF4J MDC or Logback's StructuredArguments) so your log aggregator can filter by message ID or recipient domain
  • Log at INFO for successful sends, WARN for retryable failures (429, 5xx), ERROR for permanent failures (400, 401)

Rendering HTML email content

Spring Boot includes Thymeleaf by default in the spring-boot-starter-thymeleaf starter. Use it to render email templates the same way you'd render a web page.

Create templates in src/main/resources/templates/email/ and render them with SpringTemplateEngine.process(). Pass the rendered HTML string as the html field in your API request. This keeps your email templates in version control and testable alongside your application code.

If you prefer not to add Thymeleaf, a simple String.format() or text block with variable interpolation works fine for straightforward transactional emails like password resets and order confirmations.

Webhook handling

Set up a webhook endpoint in your Spring Boot application to receive delivery events from Envello. These events tell you whether a message was delivered, bounced, or generated a complaint.

  • Create a @RestController with a @PostMapping endpoint that accepts the webhook payload
  • Verify the HMAC-SHA256 signature on every incoming webhook before processing it
  • Handle event types: 'delivered' (log success), 'bounced' (suppress the address in your application), 'complained' (suppress and flag for review)
  • Return 200 immediately and process the event asynchronously; Envello retries on non-2xx responses with exponential backoff

Testing

For unit tests, mock the RestClient and verify the request payload: correct recipient, correct subject, correct rendered HTML. Use Envello's test mode for integration tests where you want to validate the full flow without delivering to real inboxes.

Spring Boot's @WebMvcTest and MockRestServiceServer make it straightforward to test the email service without making real HTTP calls. Verify the Authorization header, Content-Type, and request body shape in your tests.

Complete example

Putting it all together: a Spring Boot service that sends a password reset email, handles errors, logs the outcome, and runs asynchronously so the web request returns immediately.

  • PasswordResetService calls EmailService.sendEmail() with the rendered reset template
  • EmailService makes the HTTP POST with RestClient, handles errors, and logs the result
  • @Async ensures the controller returns 200 to the user without waiting for the email API
  • WebhookController receives delivery events and updates the application's send log
Free tool

Check your domain's SPF, DKIM, and DMARC records

Paste in a domain and see what's missing, plus the exact DNS records to fix it. Free, no account needed.

Check your domain →
Envello

EU-hosted transactional email, done right by default.