> Как структурировать уведомления (Python)

Уровень: senior · Роль: backend · Язык: Python · Категория: Технические вопросы

Компании: Международный аэропорт Шереметьево

Стек: Python

> Пример ответа

Короткий ответ

Структурирование уведомлений - это проектирование модели данных, каналов доставки и шаблонов. Основные подходы: единая таблица notifications с полями type, channel, status, payload; отдельные таблицы под разные типы; event-driven архитектура через message broker. Ключевые trade-off: гибкость против производительности, нормализация против денормализации. Для Python-бэкенда типично использование SQLAlchemy, Celery для асинхронной отправки и шаблонизаторов типа Jinja2.

Подробное объяснение

Основные аспекты структурирования уведомлений:

Модель данных: центральная сущность Notification с полями:

  • id, user_id, type (email, push, sms, in-app)
  • channel (как доставлять)
  • status (pending, sent, failed, delivered, read)
  • payload (JSON с данными для шаблона)
  • created_at, sent_at, read_at

Типы уведомлений: системные (ошибки, алерты), пользовательские (сообщения, лайки), транзакционные (подтверждения, счета). Для каждого типа - свой шаблон и приоритет.

Каналы доставки: email (SMTP, SendGrid), push (FCM, APNs), SMS (Twilio), in-app (WebSocket, polling). Каждый канал - отдельный провайдер с интерфейсом send(notification).

Очереди и асинхронность: уведомления не должны блокировать основной поток. Используется Celery + Redis/RabbitMQ. Задача send_notification ставится в очередь, worker обрабатывает.

Шаблонизация: Jinja2 или string.Template для генерации контента. Шаблон выбирается по type, данные берутся из payload.

Дедупликация и rate limiting: предотвращение спама - проверка на повторные уведомления одного типа за период, ограничение частоты на пользователя.

Ретраи и обработка ошибок: при сбое провайдера - exponential backoff, максимум N попыток, затем статус failed и алерт.

Мультитенантность: если система обслуживает несколько клиентов - поле tenant_id, изоляция данных.

На практике

Для senior-позиции важно показать понимание масштабирования:

  • Горизонтальное масштабирование: worker'ы Celery можно реплицировать, но нужна идемпотентность задач.
  • Хранение: для больших объемов - партиционирование таблицы по created_at, архивация старых записей.
  • Мониторинг: метрики по количеству отправленных/упавших, latency доставки, алерты на аномалии.
  • A/B тестирование шаблонов: версия шаблона в payload или отдельное поле.
  • Приоритизация: срочные уведомления (пароли, 2FA) - отдельная очередь с высоким приоритетом.

Пример архитектуры:

API → создает Notification (status=pending) → Celery task → worker → provider.send() → update status

Пример кода

PYTHON
from sqlalchemy import Column, Integer, String, JSON, DateTime, Enum
from sqlalchemy.ext.declarative import declarative_base
import enum
Base = declarative_base()
class NotificationStatus(enum.Enum):
PENDING = "pending"
SENT = "sent"
FAILED = "failed"
READ = "read"
class Notification(Base):
__tablename__ = "notifications"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, index=True)
type = Column(String(50)) # "welcome_email", "like_notification"
channel = Column(String(20)) # "email", "push", "sms"
status = Column(Enum(NotificationStatus), default=NotificationStatus.PENDING)
payload = Column(JSON) # {"user_name": "Alice", "post_id": 123}
created_at = Column(DateTime, server_default=func.now())
sent_at = Column(DateTime, nullable=True)
PYTHON
# celery task
from celery import Celery
from jinja2 import Template
app = Celery("notifications", broker="redis://localhost:6379/0")
TEMPLATES = {
"welcome_email": Template("Hello {{ user_name }}! Welcome to our service."),
"like_notification": Template("{{ user_name }} liked your post #{{ post_id }}"),
}
@app.task(bind=True, max_retries=3)
def send_notification(self, notification_id):
notification = db_session.query(Notification).get(notification_id)
if not notification:
return
template = TEMPLATES[notification.type]
content = template.render(**notification.payload)
try:
provider = get_provider(notification.channel)
provider.send(notification.user_id, content)
notification.status = NotificationStatus.SENT
notification.sent_at = datetime.utcnow()
except Exception as exc:
notification.status = NotificationStatus.FAILED
raise self.retry(exc=exc, countdown=60 * 2 ** self.request.retries)
db_session.commit()

Как отвечать на собеседовании

Начни с высокоуровневого описания: модель данных, каналы, асинхронность. Затем углубляйся в детали, которые показывает интервьюер. Обязательно упомяни trade-off: например, единая таблица проще, но сложнее индексировать по типу; отдельные таблицы - лучше для разных схем, но сложнее в обслуживании.

Покажи понимание edge cases: дубликаты, сбои провайдера, изменение шаблонов без деплоя. Спроси про требования: какой объем, какие каналы, нужна ли аналитика доставки. Это покажет системное мышление.

Если интервьюер спрашивает про конкретный сценарий (например, "как обработать 10k уведомлений в секунду") - говори про партиционирование, шардирование, батчинг, отдельные очереди по приоритетам.

Что проверяет интервьюер

  • Умение проектировать модель данных под реальные требования.
  • Понимание асинхронных паттернов и очередей.
  • Знание идемпотентности и обработки ошибок.
  • Способность видеть trade-off и обосновывать выбор.
  • Практический опыт с Python-стеком (SQLAlchemy, Celery, шаблонизаторы).
  • Навыки декомпозиции задачи на компоненты.

Типичные ошибки

  • Синхронная отправка уведомлений в request-response цикле - блокирует API.
  • Отсутствие статусов - невозможно отследить доставку и ретраить.
  • Хранение HTML/текста прямо в БД вместо шаблонов - сложно менять без миграций.
  • Игнорирование дедупликации - пользователь получает 10 одинаковых писем.
  • Не учитывать rate limits провайдеров - бан аккаунта в SendGrid.
  • Одна очередь для всех типов - срочные уведомления ждут за массовыми рассылками.
  • Отсутствие мониторинга - падение доставки замечают через жалобы пользователей.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью