Как ответить
Механизм гарантированной доставки email строится на паттерне Outbox (транзакционный вывод) и событийной модели со статусами. Суть: запись письма в БД с начальным статусом «pending» происходит в рамках той же транзакции, что и бизнес-операция. Отдельный процесс (scheduler или consumer) читает новые pending-письма, отправляет через SMTP и фиксирует результат.
Статусы письма в БД:
pending— только создано, не отправленоsending— процесс взял в работу (с lock на случай параллельных обработчиков)sent— успешно отправлено, подтверждено почтовым серверомfailed— ошибка отправки (временная или постоянная)
Подписка на события: После отправки публикуется событие EmailSent или EmailFailed. Подписчики (например, сервис уведомлений) обновляют статус в основной таблице и, при необходимости, запускают retry. Для повторных попыток используется exponential backoff и максимальное количество retry (обычно 3–5).
Пример упрощённой структуры:
// Таблица outbox_emails
id UUID PRIMARY KEY,
body JSONB,
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP,
sent_at TIMESTAMP,
retry_count INT DEFAULT 0,
lock_until TIMESTAMP
// Псевдокод обработчика
function processOutbox() {
emails = db.query(
"UPDATE outbox_emails SET status='sending', lock_until=NOW()+30s
WHERE status='pending' AND (lock_until IS NULL OR lock_until < NOW())
RETURNING *"
)
foreach email in emails {
try {
smtp.send(email.body)
db.update(id, status: 'sent', sent_at: NOW())
eventBus.publish(EmailSent, {id, timestamp: NOW()})
} catch (ex) {
db.update(id, status: 'failed', retry_count++)
eventBus.publish(EmailFailed, {id, reason: ex.message})
}
}
}Ключевые моменты:
- Транзакционная консистентность — письмо сохраняется атомарно с бизнес-данными.
- Идемпотентность — повторная доставка одного письма не должна приводить к дубляжам (проверка по idempotency_key).
- Мониторинг — метрики: кол-во pending, failed, время задержки.
- Обработка ошибок — временные сбои (таймаут) → retry, постоянные (невалидный адрес) → не retry, а логирование и алерт.