Проектирование уведомлений: рабочие приёмы без лишнего шума
Уведомления ломаются не из-за отсутствия отправки, а из-за повторов, гонок, неверных приоритетов и отсутствия контроля согласий. Разбираем архитектуру, каналы, идемпотентность и метрики, которые помогают не превратить продукт в спам.

Сервис может отправить письмо, push или сообщение в мессенджер за миллисекунды, но это ещё не значит, что уведомление дойдёт, будет прочитано и не появится дважды. Рабочая система уведомлений отделяет бизнес-событие от доставки, хранит состояние попыток, учитывает настройки пользователя и умеет остановиться, если канал недоступен. Ниже — практики для backend- и продуктовых команд, независимо от языка и фреймворка; примеры опираются на HTTP API и Web Push по RFC 8030, а не на конкретную библиотеку.
Сначала разделите событие и уведомление
Одна из самых дорогих ошибок — отправлять сообщение прямо из обработчика бизнес-операции. Например, код оплаты сразу вызывает API почтового провайдера. Если провайдер отвечает медленно, транзакция заказа тоже тормозит. Если внешний вызов завершился ошибкой после фиксации платежа, система не знает, нужно ли повторять отправку. Если повторить весь запрос, можно случайно создать заказ или списать деньги второй раз.
Вместо этого используйте две сущности:
- Событие — факт, который произошёл в системе: заказ оплачен, документ подписан, лимит почти исчерпан.
- Уведомление — конкретная попытка донести этот факт до конкретного пользователя через выбранный канал.
Событие не должно содержать готовый рекламный текст и зависеть от email-шаблона. Оно описывает факт и идентификаторы: тип события, объект, пользователя, время и уникальный ключ. На отдельном шаге система проверяет настройки, выбирает канал, строит текст и создаёт задачу доставки.
Надёжная доставка — это не «мы вызвали API провайдера». Это доказуемая цепочка: событие зафиксировано, попытка создана, результат получен, повтор разрешён или запрещён по понятному правилу.
Идемпотентность — свойство операции, при котором повторное выполнение не меняет результат после первого успешного выполнения. Для уведомлений это означает: один и тот же ключ события и канала не создаёт бесконечный ряд одинаковых сообщений.
Выберите способ доставки без иллюзий
На практике встречаются три схемы. Они отличаются не только задержкой, но и тем, где окажется ошибка и кто будет отвечать за повтор.
| Подход | Как работает | Цена решения | Когда использовать |
|---|---|---|---|
| Синхронный вызов | Бизнес-запрос сразу обращается к провайдеру | Задержка и сбой канала влияют на основной запрос; повтор сложно контролировать | Только для некритичных внутренних сценариев с коротким тайм-аутом |
| Таблица исходящих событий | Событие и запись о будущей отправке фиксируются в одной транзакции, отдельный worker доставляет сообщение | Нужны worker, очистка старых записей и мониторинг очереди | Большинство транзакционных уведомлений |
| Брокер сообщений | Событие публикуется в очередь или топик, потребители формируют доставки | Появляются схемы сообщений, повторная доставка, dead-letter queue и эксплуатационная нагрузка | Несколько потребителей, большой поток, независимое масштабирование |
Для небольшого монолита таблица исходящих событий обычно даёт подходящее соотношение. Она не требует отдельной распределённой платформы, но уже отделяет пользовательский запрос от внешнего API. Брокер имеет смысл, когда уведомления обрабатывают разные сервисы, поток нельзя обслужить одним worker или нужна независимая повторная доставка.
Сделайте запись о доставке частью модели данных
Минимальная запись должна позволять ответить на пять вопросов: что отправляем, кому, по какому каналу, сколько раз пытались и чем закончилась последняя попытка. Обычно нужны идентификатор события, тип, идентификатор получателя, канал, шаблон или его версия, статус, число попыток, время следующей попытки, внешний идентификатор провайдера и причина последней ошибки.
Ключ дедупликации должен быть составным. Например, событие «счёт просрочен» для пользователя и email — не то же самое, что тот же счёт в push-канале. Не включайте в ключ случайный UUID каждой попытки: так вы сами отключите защиту от повторов.
Настройте повторы и остановку
Повторять нужно не любую ошибку. Временный тайм-аут, ответ 429 или кратковременная ошибка провайдера обычно допускают повтор. Неверный адрес, отозванная push-подписка, заблокированный получатель или ошибка в шаблоне требуют исправления данных, а не десяти одинаковых запросов.
Используйте экспоненциальную задержку с ограничением сверху и случайным разбросом. Разброс нужен, чтобы тысячи задач, упавших в одну секунду, не вернулись к провайдеру одновременно. Конкретные интервалы зависят от SLA провайдера и допустимой задержки вашего сценария, поэтому их нужно закрепить в конфигурации и проверить нагрузочным тестом, а не зашивать в код без объяснения.
- Классифицируйте ответ. Отделите временные ошибки от постоянных по HTTP-коду, коду провайдера и типу исключения. Проверка: для каждого класса есть явное решение «повторить» или «закрыть».
- Ограничьте число попыток. После последней неудачи переводите задачу в состояние permanent failure или в очередь ручного разбора. Проверка: одна проблемная подписка не создаёт бесконечный поток задач.
- Сохраняйте причину. Не ограничивайтесь полем «ошибка». Записывайте безопасный код, время, канал и внешний идентификатор, не сохраняя токены и персональные данные в открытом виде. Проверка: по записи можно понять, что исправлять.
- Разделите очередь по приоритету. Код подтверждения и рекламная подборка не должны конкурировать за один worker. Проверка: рост маркетинговой очереди не увеличивает задержку критических сообщений.
Не смешивайте каналы и их гарантии
Push, email, SMS и сообщения в мессенджере имеют разные ограничения. Push зависит от разрешения пользователя, токена устройства, операционной системы и состояния приложения. Web Push дополнительно требует корректной подписки браузера и сервис-воркера; сам факт успешного запроса к серверу не доказывает, что человек увидел сообщение.
Email обычно лучше подходит для длинного содержания и истории, но доставка зависит от домена отправителя, репутации и фильтров. SMS полезен для коротких кодов и критичных предупреждений, но стоит дороже и имеет ограничения по длине и региону. Канал нельзя выбирать только по цене: учитывайте срочность, чувствительность данных и возможность повторного просмотра.
Храните предпочтения отдельно от факта доставки
Настройка «не присылать маркетинг» не должна отключать сообщение о входе в аккаунт или изменении платежа. Для каждого типа уведомлений задайте категорию: обязательное сервисное, важное операционное, маркетинговое или информационное. Затем определите, какие каналы допустимы для категории и какие настройки пользователь может изменить.
Учитывайте часовой пояс, тихие часы, язык, частотные ограничения и наличие действующего адреса или подписки. Эти данные меняются между созданием задачи и отправкой. Поэтому проверять настройки нужно перед доставкой, а не только в момент постановки в очередь.
Текст уведомления тоже часть контракта. В нём должны быть понятны причина, действие и срок, если он есть. Не отправляйте в push полные персональные или финансовые сведения: экран блокировки может быть виден посторонним. Для чувствительных данных лучше показать нейтральное сообщение со ссылкой на защищённый экран.
Дедупликация, порядок и объединение сообщений
Повтор возникает не только из-за ретраев. Два экземпляра worker могут взять одну задачу, бизнес-событие может быть опубликовано повторно, а пользователь — дважды нажать кнопку. На уровне хранилища нужен уникальный барьер для комбинации, которая считается одной доставкой. На уровне провайдера полезно передавать собственный идемпотентный ключ, если конкретный API это поддерживает; название и срок действия такой функции нужно проверять в документации выбранного провайдера.
Порядок событий не всегда сохраняется. Сообщение «документ подписан» может прийти раньше «документ готов», если каналы и очереди разные. Не полагайтесь на порядок получения. Добавляйте версию состояния или время события и проверяйте, не устарело ли уведомление перед отправкой.
Для потоковых событий используйте объединение. Двадцать изменений в одном проекте за минуту могут превратиться в одно сообщение «В проекте 20 новых изменений». Но объединять нельзя одноразовые коды, предупреждения о безопасности и сообщения, для которых важен каждый факт. Правило агрегации должно быть частью типа уведомления, а не случайным условием в worker.
Что ломается первым
- Один общий worker. Медленный email-провайдер задерживает push и критические сообщения. Решение — отдельные очереди или хотя бы независимые лимиты и тайм-ауты.
- Повтор без классификации ошибок. Невалидные адреса возвращаются в очередь до исчерпания ресурсов. Решение — постоянные ошибки закрывать сразу и собирать отдельный отчёт.
- Логирование содержимого. В журнал попадают адреса, тексты кодов и ссылки с секретными параметрами. Решение — маскировать персональные данные, не писать токены и ограничить срок хранения логов.
- Отсутствие отмены. Пользователь удалил подписку, но старые задачи продолжают отправляться. Решение — проверять актуальные предпочтения и статус подписки перед каждым вызовом провайдера.
- Метрика только по HTTP 200. Система считает успешным сам факт принятия запроса, хотя провайдер ещё может отклонить доставку. Решение — разделять queued, accepted, delivered, failed и opened, если канал позволяет получить такие статусы.
В проде отдельно отслеживайте размер очереди, возраст самой старой задачи, долю постоянных ошибок, число повторов, задержку от события до принятия провайдером и стоимость по каналам. Для push и email не смешивайте «отправлено», «доставлено» и «прочитано»: это разные события с разной достоверностью.
Когда что выбирать
Если уведомление должно появиться после изменения записи в базе, начните с таблицы исходящих событий и одного worker. Если одно событие должны независимо обрабатывать несколько сервисов, переходите к брокеру. Если пользователю нужно подтвердить критическое действие, проектируйте основной и резервный канал, но не отправляйте резерв автоматически без политики: иначе один сбой создаст два сообщения и лишнее раздражение.
Перед запуском проверьте четыре сценария: повтор бизнес-запроса, недоступность провайдера, удаление подписки во время ожидания и всплеск одинаковых событий. Для каждого сценария должна быть видна запись в журнале, конечный статус и понятный способ восстановить доставку без ручного редактирования базы.
Что делать дальше
Сначала опишите каталог типов уведомлений: событие, получатель, приоритет, разрешённые каналы, срок актуальности, правила объединения и допустимость повтора. Затем добавьте уникальный ключ доставки и метрики очереди. После этого проверьте один критичный сценарий на отказе провайдера и только потом подключайте новые каналы или маркетинговые рассылки. Инвалидация подписок и полноценное управление согласием — отдельные темы, здесь важна лишь проверка их состояния перед отправкой.
Фитнес-тренер и нутрициолог: КБЖУ, тренировки дома и в зале, меню из РФ-продуктов.
Спросить за 12 ₽Источники: RFC 8030: Generic Event Delivery Using HTTP Push, Firebase Cloud Messaging documentation
Частые вопросы
Нужно ли сразу ставить Kafka или другой брокер сообщений?
Как не отправить одно уведомление дважды?
Что делать, если провайдер не отвечает?
Подойдут ли эти приёмы, если у меня другая версия SDK или другой провайдер?
Почему уведомление считается отправленным, но пользователь его не увидел?
Можно ли повторно отправлять сообщение после ошибки в проде?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.