На интервью на лида ответ должен быть не “я бы сделал табличку и крон”, а показать инженерное мышление: сначала уточнить требования, потом разложить систему по компонентам, потом проговорить надёжность, масштабирование, идемпотентность, ретраи, очереди, дедупликацию, observability и компромиссы.
Хороший ответ можно строить так.
1. Сначала я бы уточнил требования
Я бы начал не с архитектуры, а с границ задачи:
“Сначала я уточню, какие уведомления нужны, какие каналы, какие требования по срочности и надёжности. Потому что сервис уведомлений для маркетинговых рассылок и сервис критических security-alerts — это разные системы.”
Я бы спросил:
- Какие каналы нужны: email, SMS, push, in-app, Telegram/WhatsApp?
- Уведомления transactional или marketing?
- Есть ли критичные уведомления: OTP, password reset, fraud alert?
- Нужна ли доставка exactly once или достаточно at-least-once?
- Какие SLA: через секунды, минуты, часы?
- Нужны ли шаблоны, локализация, A/B-тесты?
- Нужны ли пользовательские настройки: mute, unsubscribe, quiet hours?
- Какой масштаб: тысячи, миллионы, десятки миллионов уведомлений в день?
- Нужно ли хранить историю уведомлений?
- Есть ли юридические требования: unsubscribe, consent, audit trail?
И дальше можно сказать:
“Для примера я спроектирую универсальный notification service для transactional и product notifications, где важна надёжная доставка, ретраи, шаблоны, пользовательские настройки и несколько каналов.”
2. Основная идея архитектуры
Я бы разделил систему на несколько частей:
Producers
|
v
Notification API
|
v
Message Broker / Queue
|
v
Notification Workers
|
+--> Template Service
+--> Preference Service
+--> Provider Adapters
|
+--> Email Provider
+--> SMS Provider
+--> Push Provider
+--> In-App Storage
То есть сервис уведомлений не должен синхронно отправлять всё прямо в момент API-запроса. Он должен принять команду, сохранить намерение отправки, положить задачу в очередь и обработать её асинхронно.
3. Входной API
Например, другие сервисы вызывают:
POST /notifications
Тело запроса:
{
"eventId": "order-123-created",
"userId": "user-456",
"type": "ORDER_CREATED",
"priority": "normal",
"channels": ["email", "push"],
"templateId": "order_created_v1",
"payload": {
"orderId": "123",
"amount": "49.99"
},
"idempotencyKey": "order-123-created-user-456"
}
Здесь важные вещи:
eventId— событие, из которого родилось уведомление.userId— кому отправляем.type— тип уведомления.channels— какие каналы использовать.templateId— какой шаблон.payload— данные для шаблона.idempotencyKey— защита от дублей.
На интервью важно сказать:
“Я бы сразу заложил idempotency key, потому что в распределённой системе producer может повторить запрос, очередь может переотдать сообщение, worker может упасть после отправки, но до сохранения статуса. Без идемпотентности сервис уведомлений быстро начнёт слать дубли.”
4. Хранилище
Я бы использовал базу для хранения состояния уведомлений.
Примерные таблицы:
notifications
- id
- idempotency_key
- user_id
- type
- priority
- status
- created_at
- updated_at
notification_attempts
- id
- notification_id
- channel
- provider
- status
- attempt_number
- error_code
- error_message
- sent_at
- created_at
notification_preferences
- user_id
- notification_type
- channel
- enabled
- quiet_hours_from
- quiet_hours_to
templates
- id
- type
- channel
- locale
- version
- subject
- body
Смысл: не просто “отправили и забыли”, а иметь историю, статус, попытки, ошибки и аудит.
5. Очередь
Между API и отправкой должна быть очередь.
Например:
Notification API -> Kafka/RabbitMQ/SQS -> Workers
Зачем очередь:
- разгружает API;
- даёт асинхронность;
- позволяет ретраи;
- сглаживает пики нагрузки;
- позволяет горизонтально масштабировать workers;
- позволяет разделять приоритеты.
Я бы сказал:
“Я бы не делал отправку уведомлений синхронной частью бизнес-транзакции. Бизнес-сервис должен быстро зафиксировать событие, а notification service должен доставить уведомление независимо и надёжно.”
Можно сделать разные очереди:
high-priority-notifications
normal-notifications
bulk-notifications
dead-letter-queue
Например:
- password reset, OTP, fraud alert — high priority;
- order update — normal;
- marketing campaign — bulk;
- сломанные сообщения — DLQ.
6. Workers
Workers читают сообщения из очереди и выполняют pipeline:
Read message
-> Validate
-> Check idempotency
-> Load user preferences
-> Resolve channels
-> Render template
-> Send via provider adapter
-> Save status
-> Retry or DLQ on failure
Хорошая формулировка:
“Я бы сделал отправку каналонезависимой. Worker не должен знать детали SendGrid, Twilio или Firebase. Он должен работать через adapter interface, чтобы можно было менять провайдера или делать fallback.”
Пример интерфейса:
interface NotificationProvider {
SendResult send(NotificationMessage message);
}
Реализации:
EmailProvider
SmsProvider
PushProvider
InAppProvider
7. Provider adapters и fallback
Для email можно использовать одного или нескольких провайдеров.
Например:
EmailAdapter
-> primary provider
-> fallback provider if primary fails
Важно не привязывать бизнес-логику к конкретному провайдеру.
На интервью можно сказать:
“Для внешних провайдеров я бы добавил adapter layer, circuit breaker, timeout, rate limiting и fallback. Внешний email/SMS provider — это нестабильная граница системы, поэтому её нельзя пускать внутрь доменной логики.”
8. Idempotency и дедупликация
Это один из самых важных пунктов.
Проблема:
- producer может отправить одно и то же событие два раза;
- очередь может доставить сообщение повторно;
- worker может упасть после отправки;
- provider может вернуть timeout, хотя сообщение фактически ушло.
Поэтому нужно:
unique(idempotency_key)
И логика:
if notification with idempotency_key already exists:
return existing notification id
else:
create notification
Для попыток отправки тоже нужна защита от дублей, особенно если канал критичный.
Можно сказать:
“Я бы проектировал систему как at-least-once delivery внутри инфраструктуры, но с идемпотентной обработкой на уровне application logic. Exactly-once в распределённых системах обычно слишком дорого и часто иллюзорно, поэтому практический путь — at-least-once plus idempotency.”
9. Retry policy
Ошибки нужно разделять.
Temporary errors:
- provider timeout
- 500 from provider
- rate limit
- network error
Permanent errors:
- invalid email
- unsubscribed user
- blocked phone number
- bad template
Для temporary errors:
retry with exponential backoff
Например:
1 min -> 5 min -> 15 min -> 1 hour -> DLQ
Для permanent errors:
mark as failed, no retry
На интервью:
“Я бы не ретраил всё подряд. Ошибки нужно классифицировать. Иначе можно перегрузить провайдера, увеличить стоимость и создать лавину повторных отправок.”
10. Rate limiting
SMS и email-провайдеры часто имеют лимиты. Плюс бизнес может хотеть ограничить частоту уведомлений.
Нужно несколько уровней rate limiting:
per provider
per channel
per user
per notification type
per tenant/client
Например:
не больше 3 marketing emails пользователю в день
не больше 1 OTP в 30 секунд
не больше N SMS в минуту через конкретного провайдера
Это важная лидерская мысль: сервис уведомлений — не просто техническая отправка, а управление нагрузкой, стоимостью и пользовательским опытом.
11. User preferences
Пользователь может отключить часть уведомлений.
Например:
Security alerts: cannot disable
Marketing: can disable
Order updates: can configure
Push: enabled
Email: disabled
SMS: only critical
Quiet hours: 23:00-08:00
Важно разделить:
mandatory notifications
optional notifications
marketing notifications
На интервью:
“Я бы не дал всем уведомлениям одинаковый статус. Security и legal notifications могут игнорировать часть пользовательских настроек, а marketing должен строго соблюдать consent и unsubscribe.”
12. Template service
Шаблоны лучше не хардкодить в producer-сервисах.
Плохо:
Order Service сам собирает email body
Хорошо:
Order Service отправляет event + payload
Notification Service сам рендерит template
Шаблоны должны поддерживать:
- версии;
- разные каналы;
- локализацию;
- preview;
- переменные;
- fallback locale;
- валидацию.
Пример:
order_created_v1_email_en
order_created_v1_email_ru
order_created_v1_push_en
order_created_v2_email_en
13. In-app notifications
Для in-app уведомлений не нужен внешний провайдер. Их можно хранить в базе:
in_app_notifications
- id
- user_id
- title
- body
- status: unread/read
- created_at
- read_at
API:
GET /users/{userId}/notifications
POST /notifications/{id}/read
Можно добавить WebSocket/SSE для real-time доставки внутри приложения.
14. Масштабирование
Масштабируются отдельно:
API instances
Queue partitions
Workers per channel
Template rendering
Provider adapters
Database
Для Kafka можно партиционировать по:
userId
tenantId
notificationType
Если важен порядок уведомлений для одного пользователя, можно использовать userId как partition key.
Но тут есть компромисс:
“Если партиционировать по userId, мы сохраняем порядок для пользователя, но можем получить hot partition для очень активных пользователей или tenant’ов. Если порядок не критичен, можно распределять шире.”
15. Consistency model
Я бы явно сказал:
“Для notification service я бы выбрал eventual consistency. Бизнес-событие может быть создано сразу, а уведомление доставлено чуть позже. Главное — надёжно зафиксировать намерение отправки и иметь механизм повторной обработки.”
То есть не надо пытаться сделать всё в одной синхронной транзакции.
Но есть важный момент:
Если бизнес-сервис создаёт заказ и потом вызывает notification API, может быть проблема:
Order created in DB
Notification API call failed
Тогда уведомление потеряно.
Лучший вариант:
Transactional Outbox Pattern
То есть бизнес-сервис пишет событие в свою outbox-таблицу в той же транзакции, что и бизнес-изменение. Потом отдельный relay публикует событие в брокер.
Order DB transaction:
- save order
- save event to outbox
Outbox relay:
- publish event to broker
Notification Service:
- consumes event
- creates notification
Это сильный пункт для лида.
16. Observability
Нужно не просто отправлять, а видеть, что происходит.
Метрики:
notifications_created_total
notifications_sent_total
notifications_failed_total
notification_delivery_latency
provider_error_rate
retry_count
dlq_size
queue_lag
template_rendering_errors
Логи:
notificationId
eventId
userId
channel
provider
status
errorCode
correlationId
Трейсинг:
business service -> broker -> notification service -> provider
Алерты:
DLQ grows
provider error rate > threshold
queue lag grows
OTP latency too high
send success rate drops
На интервью:
“Для notification service observability критична, потому что пользователь видит только факт: пришло или не пришло. Без метрик, DLQ, correlation id и истории попыток команда будет слепой.”
17. Security
Нужно защитить:
- персональные данные;
- email/phone;
- шаблоны;
- payload;
- доступ к истории уведомлений;
- unsubscribe/consent;
- audit trail.
Также нельзя позволять любому сервису отправлять что угодно кому угодно.
Нужно:
service-to-service auth
authorization by notification type
payload validation
PII masking in logs
encryption at rest where needed
18. Failure scenarios
На интервью хорошо пройтись по сбоям.
Provider недоступен
retry -> fallback provider -> DLQ
Очередь накопилась
scale workers -> prioritize high priority queue -> throttle bulk notifications
Worker упал
message redelivery -> idempotent processing
Шаблон сломан
validation before publishing template
fallback template
mark failed, no infinite retry
Пользователь получил дубль
check idempotency
provider message id
deduplication window
Нужно отправить срочное OTP
separate high-priority queue
short timeout
limited retries
no dependency on bulk pipeline
19. Что можно сказать про MVP
Для MVP я бы не строил всё сразу.
Минимально:
Notification API
DB
Queue
Worker
Email provider adapter
Basic templates
Basic retry
Basic status tracking
Basic metrics
Потом расширять:
SMS
Push
User preferences
DLQ dashboard
Fallback providers
Rate limiting
A/B tests
Localization
Campaign management
Это покажет, что ты умеешь не только рисовать “идеальную архитектуру”, но и думать стадиями.
20. Хороший итоговый ответ на интервью
Можно ответить примерно так:
“Я бы проектировал notification service как асинхронную, идемпотентную и расширяемую систему. Producers не должны напрямую отправлять email или SMS. Они должны публиковать событие или вызывать Notification API с idempotency key. Notification Service сохраняет намерение отправки, кладёт задачу в очередь, workers обрабатывают её, проверяют пользовательские настройки, рендерят шаблон и отправляют через provider adapters.
Для надёжности я бы использовал очередь, retry policy с exponential backoff, DLQ, idempotency, audit trail и status tracking. Для масштабирования — отдельные очереди по приоритетам и каналам, горизонтальное масштабирование workers, rate limiting на уровне провайдера и пользователя. Для внешних провайдеров — adapter layer, timeout, circuit breaker и fallback.
Важный момент — доставка уведомлений обычно должна быть eventually consistent. Если notification создаётся из бизнес-событий, я бы использовал transactional outbox, чтобы не потерять событие между сохранением бизнес-операции и публикацией уведомления.
Также я бы обязательно заложил observability: delivery latency, success rate, provider errors, retry count, DLQ size, queue lag, correlation id и историю попыток. Без этого notification service невозможно нормально поддерживать в production.”
21. Самая сильная фраза для лида
Вот прям хорошая финальная формулировка:
“Для меня notification service — это не просто отправка сообщений. Это система надёжной доставки событий до пользователя через разные каналы, с контролем дублей, пользовательских настроек, приоритетов, отказов внешних провайдеров и наблюдаемости. Главные риски здесь — потерять уведомление, отправить дубль, перегрузить провайдера, нарушить preference пользователя или не увидеть деградацию доставки. Поэтому архитектура должна строиться вокруг асинхронности, идемпотентности, очередей, ретраев, DLQ и observability.”
Вот это звучит уже как ответ лида, а не просто разработчика.