DEV Community

Gabrielle Eduarda
Gabrielle Eduarda

Posted on

Integrating Amazon SES into a .NET Backend Using Clean Architecture

Why Clean Architecture Matters

Clean Architecture is a design philosophy introduced by Robert C. Martin (Uncle Bob) that enforces strict separation between the core business logic and external dependencies.

It is built on three key ideas:

Independence from frameworks – business logic should not depend on frameworks or libraries.

Testability – core logic should be testable without infrastructure.

Separation of concerns – code should be organized around behavior, not technology.

In practice, this leads to four main layers:

/src
├── Domain // Entities, Value Objects, Business Rules
├── Application // Use Cases, Interfaces, DTOs
├── Infrastructure // Database, Email, External APIs
└── WebApi // Controllers, Dependency Injection

Each layer depends only on the layers inside it, never outward.

Understanding Amazon SES

Amazon Simple Email Service (SES) is a cloud-based email sending service that provides:

High deliverability and scalability

API and SMTP interfaces

SPF, DKIM, and DMARC authentication

Cost-effective pricing ($0.10 per 1,000 emails)

In a Clean Architecture project, SES belongs to the Infrastructure layer, not the Application or Domain layer.
The business logic should know what to do (send an email), not how it’s done.

Step 1: Define the Abstraction in the Application Layer

The Application layer defines contracts — interfaces that describe what needs to be done without specifying the implementation.

Create an interface for sending emails:

namespace Application.Common.Interfaces
{
public interface IEmailSender
{
Task SendAsync(string to, string subject, string body, bool isHtml = true);
}
}

This abstraction allows the Application layer to send emails without knowing that SES exists.

Step 2: Implement the Service in the Infrastructure Layer

In the Infrastructure layer, we create the actual SES integration using the AWS SDK for .NET.

using Amazon.SimpleEmail;
using Amazon.SimpleEmail.Model;
using Application.Common.Interfaces;

namespace Infrastructure.Email
{
public class SesEmailSender : IEmailSender
{
private readonly IAmazonSimpleEmailService _sesClient;
private readonly string _fromAddress;

    public SesEmailSender(IAmazonSimpleEmailService sesClient, string fromAddress)
    {
        _sesClient = sesClient;
        _fromAddress = fromAddress;
    }

    public async Task SendAsync(string to, string subject, string body, bool isHtml = true)
    {
        var request = new SendEmailRequest
        {
            Source = _fromAddress,
            Destination = new Destination
            {
                ToAddresses = new List<string> { to }
            },
            Message = new Message
            {
                Subject = new Content(subject),
                Body = new Body
                {
                    Html = isHtml ? new Content(body) : null,
                    Text = isHtml ? null : new Content(body)
                }
            }
        };

        await _sesClient.SendEmailAsync(request);
    }
}
Enter fullscreen mode Exit fullscreen mode

}

This class uses dependency injection to receive the SES client and configuration.

Step 3: Register the Implementation in Dependency Injection

In the WebApi or composition root, configure the dependencies:

using Amazon.SimpleEmail;
using Application.Common.Interfaces;
using Infrastructure.Email;

builder.Services.AddAWSService();
builder.Services.AddSingleton(sp =>
{
var sesClient = sp.GetRequiredService();
var config = sp.GetRequiredService();
var fromAddress = config["Email:From"];
return new SesEmailSender(sesClient, fromAddress);
});

Now any part of the system can use IEmailSender through DI, without referencing SES or AWS directly.

Step 4: Use the Abstraction in an Application Use Case

Inside the Application layer, you can use the IEmailSender in your use cases:

using Application.Common.Interfaces;

namespace Application.Users.Commands
{
public class SendWelcomeEmail
{
private readonly IEmailSender _emailSender;

    public SendWelcomeEmail(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }

    public async Task ExecuteAsync(string userEmail, string userName)
    {
        var subject = "Welcome to our platform!";
        var body = $"<p>Hello {userName},</p><p>We’re excited to have you onboard.</p>";

        await _emailSender.SendAsync(userEmail, subject, body);
    }
}
Enter fullscreen mode Exit fullscreen mode

}

This use case can be tested without SES, because you can mock the IEmailSender interface.

Step 5: Testing Without AWS

To make the Application layer fully testable, you can use a mock implementation:

public class FakeEmailSender : IEmailSender
{
public List<(string To, string Subject, string Body)> SentEmails { get; } = new();

public Task SendAsync(string to, string subject, string body, bool isHtml = true)
{
    SentEmails.Add((to, subject, body));
    return Task.CompletedTask;
}
Enter fullscreen mode Exit fullscreen mode

}

Now your unit tests can verify that the correct email logic was executed without touching AWS.

Step 6: Monitoring and Observability

When running in production, integrate SES with AWS observability tools:

CloudWatch Metrics: monitor send rates, bounces, and complaints.

SNS Notifications: get real-time feedback for delivery events.

CloudTrail: track API calls for auditing and compliance.

SES supports automatic reputation management and DKIM authentication, reducing the risk of your emails being flagged as spam.

Benefits of This Approach

By integrating SES through Clean Architecture, you achieve:

Low coupling: business logic is not tied to AWS.

Testability: easy to mock and verify in unit tests.

Flexibility: switch to another provider (SendGrid, Mailgun) with minimal changes.

Maintainability: infrastructure logic is centralized and isolated.

Scalability: SES can handle millions of emails per day without additional configuration.

Conclusion

Integrating Amazon SES into a .NET backend using Clean Architecture ensures that email functionality remains modular, testable, and future-proof.

Your business rules never depend on cloud SDKs or third-party services.
Instead, they depend on stable, well-defined contracts that make your system easier to evolve and maintain.

Clean Architecture isn’t just about layers — it’s about clarity, independence, and long-term reliability.
With Amazon SES as your email infrastructure and .NET as your foundation, you can build backend systems that scale cleanly and deliver with confidence.

Top comments (0)