Ссылка
нажмите — покажем
нажмите — покажем
System Design: backend
Задача с реального собеса Яндекса. Условие: приходит уведомление — id, тип (EMAIL/SMS/PUSH), получатель, текст. У получателя есть разрешённые каналы и блок-лист отправителей. Нужно отфильтровать уведомление и не отправить дубль, если такое же уже было за последние 24 часа. Хранилище реализовывать не надо — только контракты и логика. Погнали.
Понятное дело, что мы уже люди опытные и не побежим писать код, думать над DTO — мы сразу будем думать над системой проверок уведомлений. Для этого сначала нужно понять, какой информации нам не хватает.
Определим рамки задачи
Сначала границы: какие каналы поддерживаем, откуда берём настройки получателя, что считаем «таким же» уведомлением для дедупа.
Например, что значит «такое же»? «Ваш заказ №123 готов» и «Ваш заказ №124 готов» — дубль или нет? А если одно пришло по EMAIL, а второе по SMS?
Поэтому уточняем:
* дедуп общий для всех каналов или отдельный;
* что именно считается одинаковым уведомлением;
* что считается успешной отправкой;
* какие ожидаются RPS.
Но главный вопрос один: где мы помним, что уже отправляли? Это относится к устройству хранилища, которое нам сказали не реализовывать, но без этой информации нам всё же не обойтись — дальше вернёмся к этому.
Ядро решения
Ключевая мысль: фильтрация — это цепочка независимых проверок, через которую уведомление либо проходит целиком, либо отсеивается на первом же несоответствии.
Не один раздутый if, а pipeline из мелких фильтров:
- канал разрешён у получателя;
- отправитель не в блок-листе;
- уведомление не дубль за последние 24 часа.
Первый же фильтр, сказавший «нет», отсекает уведомление — остальные не гоняем. Такую систему легко расширять: новый фильтр = ещё одно звено, а не правка портянки.
Возвращаемся к дедупликации
Дедуп за 24 часа означает, что система должна хранить след недавно отправленных уведомлений. Ключ вроде: recipient + type + id. А по нему — время отправки. Пришло новое — считаем ключ и смотрим, существует ли такой за последние 24 часа. И два неочевидных момента.
Первый — что именно входит в notificationIdentity. Сырый текст не всегда подходит: если в нём динамические данные, побайтовое сравнение может дать неправильную семантику. Поэтому сначала выясняем у интервьюера, что бизнес считает дублем. Второй — TTL. Записи должны сами протухать через 24 часа, иначе хранилище будет расти бесконечно.
Где сыпятся
Порядок фильтров: дешёвые и отсекающие проверки — вперёд. Канал и блок-лист — настройки. Дедуп — поход в стор. Глупо лезть в стор за дедупом, если уведомление и так отсечётся выключенным push.
Но есть ещё одна ловушка — гонка на дедупе. Поэтому check() и запись факта отправки должны быть атомарными. Например, через операцию tryReserve(key, ttl): первый запрос получает true, второй — false. А дальше интервьюер вполне может спросить: что будет, если мы зарезервировали уведомление, а внешний провайдер упал? Вот тут уже нужно отдельно определить семантику: защищаемся от повторного вызова сервиса или гарантируем отсутствие повторной доставки пользователю? Это разные задачи.
Контракты, а не реализация
Откуда берутся настройки и история отправок — прячем за интерфейсами: SettingsProvider, HistoryStore, и неважно, Redis там, PostgreSQL или внешний сервис. На собесе мы проектируем логику, а не привязываемся к конкретному хранилищу.
Что тут на самом деле проверяют
Проверяют одно: видишь ли ты, что это не «класс Notification с четырьмя полями», а конвейер фильтров с памятью о недавних отправках. Красивые модели нарисует и джун — и утонет в них, не дойдя до логики. Инженер сразу думает: что проверяем → в каком порядке → где храним состояние → что считаем дублем → что произойдёт при гонке.
А дальше строит pipeline от дешёвых проверок к дорогим, прячет источники данных за контрактами и понимает, что дедупликация — это не просто get() в Redis.
Подписаться: @codeof_art
Вступить в чатик: @code_of_art
1.5K ·