Email-рассылка как распределённая система
На бумаге сервис рассылки выглядит как три сущности: список адресов, кампания, кнопка Send. На практике доставка почты — это асинхронная интеграция с внешними MTA (Gmail, Outlook, корпоративный Exchange), где ошибки приходят с задержкой, повторная отправка опасна, а «успех» в API провайдера ещё не значит попадание во входящие.
Эта глава собирает типичный продакшн-контур в одном месте: для system design, для code review и как мост между SMTP в коде и картой system design. Сквозной пример в формате собеседования — ниже в 143.
Такой контур встречается в транзакционной почте (сброс пароля, чеки), в newsletter-платформах и в open-source вроде Camp (Go). Продукты различаются UI и фичами, а инженерные задачи доставки — одни и те же.
Ложный CRUD и реальная граница системы
| Слой, который видит продукт | Что происходит в инфраструктуре |
|---|---|
| Сохранить подписчиков в БД | Валидация, double opt-in, согласие на обработку данных |
| Создать кампанию и нажать Send | Постановка N×M задач в очередь, лимиты, state machine |
| «Письмо отправлено» в UI | Принято ESP/SMTP; доставка и открытие — отдельные события |
| Статистика открытий | Отдельные HTTP endpoints, боты, блокировщики трекеров |
Пока рассылка идёт на десяток адресов через один SMTP-ящик, хватает скрипта. После выхода в прод с тысячами получателей и разными доменами @gmail.com / @company.com появляются очередь, пул воркеров, слой доставки (адаптер к SES, SendGrid, Mailgun, Postmark или своему MTA) и конечный автомат на каждое письмо или пару «кампания + получатель».
Типовая архитектура
API принимает «запустить кампанию», фиксирует намерение в БД и не шлёт письма в HTTP-запросе. Outbox (паттерн) гарантирует, что запись о кампании и задача в очереди появятся вместе. Воркеры масштабируются отдельно; их узкое место — лимиты ESP и репутация домена, а не CPU API.
Скелет «API → primary → очередь → воркеры» — тот же, что в типовом контуре system design и §4 очередей.
Конечный автомат сообщения
Статус хранят на уровне отправки одному получателю (или одного message id), а не только на уровне кампании «в целом».
| Статус | Смысл для продукта |
|---|---|
pending / sending | Можно показывать «в очереди»; при падении воркера задача возвращается в очередь |
accepted | Провайдер принял; во «входящие» письмо может не попасть (спам, DMARC) |
bounced_hard | Адрес недействителен — больше не слать без ручной проверки |
bounced_soft | Временный отказ — ограниченный retry |
suppressed | Глобальный запрет на адрес (bounce, complaint, ручной unsubscribe) |
complained | Жалоба «спам» — хуже hard bounce для репутации домена |
Кампания в UI — агрегат: sent = count(delivered) + count(accepted), failed = count(suppressed) + count(bounced_hard).
Bounces и suppression list
Bounce — уведомление о недоставке: SMTP-ответ при отправке или asynchronous bounce (отдельное письмо от MTA получателя на ваш bounce-адрес / webhook ESP).
| Тип | Типичные причины | Политика |
|---|---|---|
| Hard bounce | Несуществующий ящик, домен не принимает почту | Сразу в suppression; retry бессмысленен |
| Soft bounce | Почтовый ящик переполнен, greylisting, временный 4xx | Exponential backoff, cap попыток (например 3–5 за 72 ч) |
| Complaint | Пользователь нажал «Спам» у провайдера | Немедленный suppress; расследование контента и частоты |
Suppression list — отдельное хранилище (таблица или Redis set) email-адресов и доменов, куда ни одна кампания больше не ставит задачу. Проверка выполняется до постановки в очередь и перед вызовом delivery layer.
Каждая попытка бьёт по репутации отправляющего домена (sender score). ESP могут ограничить или заблокировать аккаунт. Suppression — часть устойчивости, а не «удобство CRM».
Retry policy
Retry относится к инженерии устойчивости (design/2136), а не к циклу for вокруг sendmail.
| Ситуация | Retry? | Заметка |
|---|---|---|
| Таймаут к ESP, 5xx провайдера | Да | С jitter и max attempts |
| Soft bounce | Да | С увеличением интервала |
| Hard bounce, complaint | Нет | Только suppress |
Успешный accepted, потом duplicate worker | Нет повторной отправки | Нужна идемпотентность (ниже) |
| Rate limit 421 / 450 от Gmail | Да | С глобальным throttle по домену получателя |
Повторять имеет смысл только идемпотентные шаги (design/213): «поставить задачу», «отметить accepted», но не «отправить письмо без ключа дедупликации».
Идемпотентность и сбой воркера
Типичный инцидент: воркер отправил письмо через ESP, упал до commit статуса в БД; при рестарте очередь отдаёт задачу снова — пользователь получает дубликат.
Защита:
- Уникальный ключ отправки —
(campaign_id, recipient_id)илиmessage_id(UUID), передаётся в ESP как idempotency key / custom header, где API это поддерживает. - Состояние «sending» с lease — только один воркер держит lock на строку; по TTL lock снимается для recovery.
- At-least-once очередь (брокеры) + идемпотентный consumer: повторная доставка задачи не создаёт второе письмо, если статус уже
acceptedили выше.
Транзакционный outbox связывает «кампания запущена» и «задачи созданы»; без него возможны кампании без писем или письма без записи в БД.
SPF, DKIM, DMARC — «тихая» недоставка
Ошибки аутентификации редко ломают ваш API. Письмо уходит, а попадает в спам или отбрасывается на стороне получателя.
| Механизм | Где настраивается | Что проверяет получатель |
|---|---|---|
| SPF | DNS TXT у домена отправителя | Разрешён ли этот IP/хост слать от имени домена |
| DKIM | DNS TXT + подпись заголовков письма | Целостность письма и соответствие домену |
| DMARC | DNS TXT | Политика при несовпадении SPF/DKIM (none / quarantine / reject) + отчёты |
Записи TXT для SPF/DKIM — в настройке DNS; порты submission — в справочнике почты.
На staging проверяйте заголовки Authentication-Results, отчёты DMARC в почте администратора и тестовые ящики (mail-tester, встроенные postmaster-консоли Gmail/Outlook). Метрика «accepted от ESP» без мониторинга spam rate обманывает дашборд.
Throttling и домен получателя
Лимиты бывают на трёх уровнях:
| Уровень | Пример | Реакция |
|---|---|---|
| Ваш API / воркеры | 1000 задач/с | Token bucket на enqueue |
| ESP (SES, SendGrid) | N писем/сек на аккаунт | Очередь + backoff |
| Почтовый провайдер получателя | Gmail: плавный ramp для нового домена | Per-domain лимитер (gmail.com, outlook.com) |
Один глобальный sleep(1) между письмами не спасает: домен small-corp.ru может принимать быстро, а Gmail — нет. Часто вводят несколько очередей или rate-limit keys по MX-домену получателя.
Связь с rate limiting в архитектуре и троттлингом в администрировании.
Webhooks от ESP
Провайдеры шлют HTTP POST о событиях: delivered, bounced, complained, opened (если включено на их стороне). Это тот же паттерн, что входящие webhooks:
- проверка подписи (HMAC shared secret);
- идемпотентность по
event_idпровайдера; - быстрый
200 OKи обработка в фоне (очередь), чтобы не истекал timeout повторов у ESP; - защита от replay (timestamp + nonce).
Обработчик обновляет state machine и suppression list; без него soft/hard bounce из реального мира не попадут в вашу БД.
Open tracking, клики и отписка
| Функция | Реализация | Риски |
|---|---|---|
| Open tracking | Пиксель 1×1 на вашем HTTPS | Блокировщики, превью почтовых клиентов, завышенные «открытия» |
| Click tracking | Редирект через ваш домен | Сломанные ссылки при падении трекера; нужен fallback |
| Unsubscribe | Заголовок List-Unsubscribe + URL + опционально mailto: | Юридические требования (CAN-SPAM, GDPR); отписка должна работать без логина по подписанному токену |
Эндпоинты трекинга и отписки — отдельная поверхность атаки (подбор токенов, DDoS). Нужны rate limit, короткий TTL токена, логирование без PII в открытом виде.
Транзакционные письма (сброс пароля) обычно без marketing open-tracker; маркетинговые — с явным согласием в коммуникациях и GDPR.
Слой доставки — свой SMTP или ESP
| Подход | Когда уместен | Что вы всё равно пишете сами |
|---|---|---|
| ESP (SES, SendGrid, Mailgun, Postmark) | Почти всегда в прод | Очередь, state machine, webhooks, suppression |
| Свой MTA (Postfix + IP, warmup) | Крупный объём, своя доставляемость | Репутация IP, bounce mailbox, FBL, мониторинг blacklists |
| smtplib в приложении | Прототип, внутренние уведомления | Throttling, retry, bounces — вручную |
В Python-главе разобраны SMTP-коды и bulk-отправка; для продакшн-объёма там же указан переход на очередь и ESP с готовыми bounces.
Наблюдаемость
Метрики, без которых рассылку слепо эксплуатировать:
| Метрика | Зачем |
|---|---|
| Глубина очереди / age of oldest message | Задержка кампании, нехватка воркеров |
| Отправок/сек по MX-домену | Поймать ban от Gmail |
| Доля hard/soft bounce, complaint rate | Репутация домена |
| Retry count, DLQ size | Политика устойчивости |
Расхождение accepted vs webhook delivered | Проблемы auth / спам |
Алерты — на рост complaint rate и DLQ, а не только на 5xx API.
Чек-лист для system design interview
Краткий порядок ответа (полный каркас — 143):
- NFR — писем/день, допустимая задержка кампании, нужна ли exactly-once доставка письма пользователю.
- High-level — API + DB + outbox + очередь + workers + ESP; webhooks обратно.
- Deep dive — state machine, suppression, идемпотентность, per-domain throttle.
- Отказы — падение ESP (circuit breaker, пауза кампании), дубликаты при retry воркера.
- Compliance — unsubscribe, хранение согласий, разделение transactional / marketing.
См. также
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). 52 элемента 33 элемента Обычно проектирование применяется к каким-то планам, схемам, моделям или расчётам, которые описывают будущий объект, включая характеристики, функции, инженерные решения. Архитектурные решения, касающиеся распределения компонентов и организации их взаимодействия, определяют фундаментальные свойства системы: её масштабируемость, отказоустойчивость, сложность. Это достигается через инверсию зависимостей — принцип, согласно которому высокоуровневые модули не должны зависеть от низкоуровневых; оба должны зависеть от абстракций. Компонентно-ориентированная архитектура - согласованность версий общих модулей и управление зависимостями между сервисами. Как резать монолит без "большого взрыва": пять вопросов перед стартом, анализ, Strangler, DDD-контексты, данные, саги и метрики успеха. Инфраструктура — это множество решений, инкапсулированных в сервисы, каждое из которых накладывает ограничения и открывает возможности. Классификация типов классов в ООП - семантика имён, роли объектов и разделение ответственности в проекте. Построение систем на классах и объектах - модель предметной области, границы ответственности и связи между сущностями. Доменная модель - как отразить предметную область в ПО, выделить сущности и зафиксировать правила бизнес-логики. Тактические строительные блоки Domain-Driven Design: Entity, Value Object, Aggregate Root, доменные сервисы, репозитории, фабрики и события — какие классы в каком слое и чем они отличаются от DTO и контроллеров.Проектирование
Паттерны проектирования
Основы проектирования и архитектуры программного обеспечения
Архитектурные стили и их применение
Стили внутренней организации кода
Принципы компонентно-ориентированной архитектуры
Стратегии декомпозиции монолитных систем
Влияние инфраструктуры на архитектурные решения
Классификация типов классов в объектно-ориентированном проектировании
Построение систем на основе классов и объектов
Доменная модель
Типы классов в DDD