Skip to content

Email Sending Service Design (HLD & LLD)

  1. Send emails to users.
  2. Support single email and bulk emails.
  3. Support HTML templates.
  4. Retry failed deliveries.
  5. Track email status (sent, delivered, failed).
  6. Support attachments.
  7. Support multiple providers (SMTP, SES, SendGrid).
  • High throughput (millions of emails/day)
  • Reliability
  • Retry & fault tolerance
  • Scalability
  • Rate limiting
  • Low latency
flowchart TD
    C[Client] --> G[API Gateway]
    G --> E[Email Service]
    E --> Q[Message Queue]
    Q --> W[Email Worker]
    W --> P[Email Provider]
    W --> D[(Status DB)]

Responsibilities:

  • Authentication
  • Rate limiting
  • Request validation
  • Routing

Example request:

POST /email/send

Payload:

{
"to": "user@example.com",
"subject": "Welcome",
"templateId": "WELCOME_TEMPLATE",
"variables": {
"name": "John"
}
}

Responsibilities:

  • Validate request
  • Apply template
  • Save email request
  • Publish message to queue

Examples:

  • Kafka
  • RabbitMQ
  • AWS SQS

Benefits:

  • Decoupling
  • Retry support
  • Backpressure handling
  • High throughput

Responsibilities:

  • Consume queue
  • Build email
  • Connect to provider
  • Send email
  • Update status
  • Retry on failure

Examples:

  • SMTP
  • AWS SES
  • SendGrid
  • Mailgun

Use a provider abstraction layer to enable failover.

Example schema:

EmailRequest
-------------
id
recipient
subject
template_id
status
retry_count
created_at
updated_at
classDiagram
class EmailController
class EmailService
class EmailQueueProducer
class EmailWorker
class EmailSender
class EmailProvider
class RetryService
class TemplateService

EmailController --> EmailService
EmailService --> TemplateService
EmailService --> EmailQueueProducer
EmailWorker --> EmailSender
EmailSender --> EmailProvider
class EmailRequest {
String id;
String to;
String subject;
String templateId;
Map<String, String> variables;
EmailStatus status;
int retryCount;
}
@RestController
class EmailController {
@Autowired
EmailService emailService;
@PostMapping("/send")
public ResponseEntity sendEmail(@RequestBody EmailRequest request) {
emailService.queueEmail(request);
return ResponseEntity.ok("Email queued");
}
}
class EmailService {
@Autowired
EmailQueueProducer producer;
@Autowired
TemplateService templateService;
public void queueEmail(EmailRequest request) {
String body = templateService.renderTemplate(
request.getTemplateId(),
request.getVariables());
request.setBody(body);
producer.publish(request);
}
}
class EmailQueueProducer {
public void publish(EmailRequest request) {
kafkaTemplate.send("email-topic", request);
}
}
class EmailWorker {
@Autowired
EmailSender sender;
@KafkaListener(topics="email-topic")
public void process(EmailRequest request) {
sender.send(request);
}
}
class EmailSender {
@Autowired
EmailProvider provider;
public void send(EmailRequest request) {
provider.sendEmail(
request.getTo(),
request.getSubject(),
request.getBody()
);
}
}
interface EmailProvider {
void sendEmail(String to, String subject, String body);
}
class SmtpEmailProvider implements EmailProvider {
public void sendEmail(String to, String subject, String body) {
// SMTP send logic
}
}
Retry 1 -> 10 sec
Retry 2 -> 1 min
Retry 3 -> 5 min

After the maximum retry count, move the message to a Dead Letter Queue (DLQ).

Respect provider limits (e.g., SES 50 emails/sec).

Approaches:

  • Token bucket
  • Queue throttling
flowchart TD
    C[Campaign Service] --> G[Generate Email Jobs]
    G --> Q[Queue]
    Q --> W[Worker Pool]

Example:

WELCOME_TEMPLATE
Hello {{name}},
Welcome to our platform!

Variables are substituted at runtime.

  • Open tracking
  • Click tracking

Implementation:

  • Tracking pixel
  • Redirect URLs

Track:

  • Email sent rate
  • Failure rate
  • Retry rate
  • Queue lag

Tools:

  • Prometheus
  • Grafana

Use a unique request identifier:

request_id

Prevent duplicate sends by checking previously processed IDs.

  • Horizontal worker scaling
  • Kafka partitioning
  • Multi-provider failover

Example:

Primary -> SES
Fallback -> SendGrid
  1. Exactly-once vs at-least-once delivery
  2. Idempotency
  3. Retry strategy and DLQ
  4. Provider failover
  5. Rate limiting
  6. Bulk campaign handling
  7. Template management
  8. Observability
  • Use asynchronous processing with a message queue for scalability.
  • Abstract email providers to support failover and vendor independence.
  • Implement retries with exponential backoff and a DLQ.
  • Enforce idempotency to avoid duplicate emails.
  • Monitor queue lag, delivery success, and retry metrics for operational health.