Email Outbox Pattern¶
Reliable async email delivery using database persistence.
Overview¶
flowchart TB
subgraph Request["Request Handler"]
A[Create AppTask] --> B[Save to DB]
end
subgraph Job["EmailSendJob"]
C[Query Pending Tasks] --> D[Render Template]
D --> E[Send via Resend]
E -->|Success| F[Mark Completed]
E -->|Failure| G[Increment Retry]
end
B -.->|Hangfire Schedule| C Why Outbox?¶
| Problem | Solution |
|---|---|
| Email API down | Tasks retry automatically |
| Request timeout | Email sends async |
| Duplicate sends | Idempotent task processing |
| Lost emails | Database persistence |
AppTask Entity¶
public class AppTask : AuditableEntity
{
public Guid TaskId { get; set; }
public TaskType Type { get; set; }
public ObjectType ObjectType { get; set; }
public TaskStatus Status { get; set; }
public TaskPriority Priority { get; set; }
public string? Data { get; set; }
public DateTime ScheduledAt { get; set; }
public DateTime? CompletedAt { get; set; }
public DateTime? FailedAt { get; set; }
public int RetryCount { get; set; }
public string? LastError { get; set; }
public Guid? UserId { get; set; }
}
Email Types (ObjectType)¶
| Type | Purpose |
|---|---|
EmailConfirmation | Verify user email address |
PasswordReset | Password recovery link |
RegistrationExpiring | Warning before registration expires |
RegistrationExpired | Notification that registration was declined |
Queueing an Email¶
var task = new AppTask
{
Type = TaskType.EmailSend,
ObjectType = ObjectType.EmailConfirmation,
Priority = TaskPriority.Normal,
Status = TaskStatus.Pending,
Data = JsonSerializer.Serialize(new EmailConfirmationData
{
UserId = user.UserId,
Token = confirmationToken,
Email = user.Email
})
};
context.Tasks.Add(task);
await context.SaveChangesAsync();
EmailSendJob Processing¶
Processes tasks one at a time in priority order:
var task = await context.Tasks
.Where(t => t.Type == TaskType.EmailSend
&& t.Status == TaskStatus.Pending
&& t.ScheduledAt <= now)
.OrderByDescending(t => t.Priority)
.ThenBy(t => t.CreatedDate)
.FirstOrDefaultAsync(ct);
On failure, tasks are rescheduled with exponential backoff until max retries exceeded.
Schedule¶
Configurable via Hangfire:JobsCron:EmailSendJob in appsettings.
Template Rendering¶
Uses Blazor template engine:
var html = await templateRenderer.RenderAsync<EmailConfirmationTemplate>(
new EmailConfirmationModel
{
UserName = user.Nickname,
ConfirmUrl = $"{appDomain}/confirm-email?token={token}",
Language = user.Language
});
Related¶
- Background Jobs — Job scheduling
- Integrations — Resend email service