Notification Service — Use Case спецификация (Tier A)
Полная Use Case спецификация Notification Service из кейса маркетплейса. Tier A: legacy / слоёная архитектура без UseCase Pattern и DDD. Контрпример к Order Service — показывает, когда DDD overkill.
Эта спека — контрпример к Order Service. Order Service — Tier C: домен, агрегаты, события, саги. Notification Service — Tier A: контроллер, сервис, репозиторий. Без UseCase Pattern, без DDD, без агрегатов. И это правильно: задача сервиса — взять входящее событие, отрендерить шаблон, отправить во внешний канал, записать результат. Никаких бизнес-инвариантов, которые надо защищать.
Цель статьи — показать, как выглядит Use Case спецификация, когда от полного шаблона остаётся скелет: 2 из 17 разделов помечены «не пишется» (Domain Events и Saga), несколько других упрощены до списков. Дальше — машинно-читаемая копия в github.com/remodov/notification-service, из которой скиллы методологии генерируют код.
Содержание
- Контекст / модуль
- Ubiquitous Language
- Domain Model
- Жизненный цикл уведомления
- Роли и права
- Бизнес-правила
- Операции (Commands)
- Queries / Read Model
- Use Cases
- UI-спецификация
- Каталог ошибок
- Интеграции
- Критерии приёмки
- Нефункциональные требования
- Стек технологий
1. Контекст / модуль
Сервис: «Notification»
На Tier A термин «Bounded Context» избыточен — Notification это не отдельная предметная область, это техническая прокладка между Kafka и провайдерами доставки. Поэтому здесь — просто описание модуля.
Отвечает за
- Подписку на доменные события Order Service (
OrderConfirmed,OrderPaid,OrderShipped,OrderDelivered,OrderCancelled,OrderRefunded,DisputeOpened,DisputeResolved). - Выбор каналов доставки по типу события (фиксированная таблица в коде).
- Загрузку шаблона из БД, подстановку переменных, отправку через SMTP-провайдер и Firebase Cloud Messaging.
- Журналирование каждой попытки отправки: статус, ошибка, время.
- Ретраи при временных ошибках провайдера (3 попытки с экспоненциальной задержкой).
- Приём webhook'ов от SMTP-провайдера про
delivered/bounced. - Админский UI: фильтрация журнала, ручной retry «упавших», просмотр исходного события.
Не отвечает за
- Бизнес-логику маркетплейса. Любые правила вида «отправляй СМС если сумма > 100к» не здесь, а в источнике события.
- Хранение настроек подписок пользователя. Tier A не делает «отписку от рассылки» — это расширение для Tier B (см. §15).
- A/B-тесты текстов писем — отдельная история, не входит в первый запуск.
- Кампании / массовые рассылки. Notification отправляет транзакционные уведомления, привязанные к событию.
- Push-сертификаты Apple/Android и токены устройств — берёт у Customer BFF через REST.
- Хранение тела письма после отправки (только метаданные: кому, когда, статус).
Соседние системы
Все связи — асинхронные кроме одной (REST в Customer BFF за токенами и email).
| Сосед | Направление | Как |
|---|---|---|
| Order Service | inbound | подписка на топик marketplace.orders.v1 (Kafka) |
| Customer BFF | outbound REST | GET /api/v1/users/{id}/contact — email + push-токены пользователя |
| SMTP-провайдер (Mailgun/SendGrid) | outbound + inbound webhook | POST /messages для отправки; webhook /notifications/email-events для bounce/delivered |
| Firebase Cloud Messaging | outbound | POST /fcm/send — fire-and-forget, без webhook |
| Admin UI / оператор | inbound REST | GET /api/v1/notifications (фильтрация), POST /retry |
Стейкхолдеры
- Владелец: команда «Платформа» (Platform team) — Notification считается инфраструктурным сервисом.
- Зависят от нас: Customer BFF (показывает inbox), оператор поддержки (читает журнал доставок при разборе жалоб).
- От кого зависим: Order Service (источник событий), Customer BFF (контакты), SMTP-провайдер, FCM.
Диаграмма C1
2. Ubiquitous Language
Глоссарий очень короткий — Notification работает с минимальным числом понятий.
| Термин | Определение |
|---|---|
| Notification | Запись в журнале: одно событие, одна попытка доставки в один канал одному адресату. Если на одно событие отправляется письмо + push — это две Notification-записи. |
| Channel | Канал доставки. На запуске два: EMAIL, PUSH. Третий (SMS) — расширение без переписывания, добавится только провайдер. |
| Recipient | Кому отправляем. Хранится userId + материализованный контакт на момент отправки (email-адрес или push-токен) — нужно для журнала: пользователь мог поменять email, а в журнале должна остаться правда. |
| Template | Шаблон сообщения в БД: ключ (order.confirmed) + язык + субъект и тело с плейсхолдерами ${var}. |
| Source event | Входящее событие из Kafka, по которому создана Notification. Сериализованный JSON хранится для дебага и retry. |
| Provider | Внешняя система доставки: Mailgun, FCM, в будущем SMS. У каждой Notification ровно один. |
| Delivery status | Статус: QUEUED, SENT, DELIVERED, BOUNCED, FAILED. См. §4. |
Намеренно нет:
- Aggregate / Entity / Value Object — Notification это row в таблице, не агрегат.
- Domain event / saga — мы не публикуем события, не координируем процессы.
- Customer / Seller — для Notification они оба просто
userId. Никакой дополнительной семантики не нужно.
3. Domain Model
На Tier A раздел сводится к ER-схеме и списку таблиц.
ER-схема
Таблицы — словами
notifications— главная таблица. Одна строка = одна попытка доставки на один канал. Хранит копию контакта на момент отправки (email мог поменяться) и копию шаблонных переменных (для retry без обращения к Customer BFF).templates— шаблоны письма. Ключ =<event_type>.<channel>(напримерorder.confirmed.email). Язык —locale. Тело может содержать${var}— подстановка простойString.replace.delivery_attempts— лог каждой попытки отправки. Изолирован отnotificationsчтобы запросы по статусу шли по индексу без сканирования большихjsonb-полей.processed_events— журнал обработанных Kafka-событий для идемпотентности консьюмера. PK поevent_id— повторная доставка от Kafka не создаст дубль уведомления.
Что нет
- Tier A не вводит модели UseCase-входов (
SendNotificationRequestи т.п.). Контроллер / сервис принимают сразу JSON-DTO. Это ОК для CRUD-сервиса. - Нет агрегатов, value objects, доменных событий. Это проектное решение, не упущение.
4. Жизненный цикл уведомления
Терминальные состояния: DELIVERED, BOUNCED, и FAILED после ручной отметки оператора.
Замечания:
- PUSH-канал не имеет webhook'а — статус автоматически становится
SENTи остаётся таким;DELIVERED/BOUNCEDдля push не достижимы. Это нормальное ограничение FCM. - Между
QUEUEDиSENTуведомление может несколько раз пройти черезdelivery_attemptsсresult=TRANSIENT_ERROR. Видимый статус остаётсяQUEUEDдо первой удачной отправки. - Ручной retry возможен только для
FAILED. ИзBOUNCEDretry бесполезен — провайдер сказал «адрес недоставляем».
5. Роли и права
Notification — внутренний сервис, у него нет публичного API для покупателей. Все REST-эндпоинты делятся на две группы:
| Endpoint | Кому |
|---|---|
POST /webhooks/email-events | SMTP-провайдер (фиксированный IP + HMAC-подпись в заголовке) |
GET /api/v1/notifications, POST /api/v1/notifications/{id}/retry | оператор поддержки (роль support-operator в Keycloak) |
ABAC отсутствует — оператор видит все уведомления независимо от того, кому они отправлялись. Это сознательное ограничение Tier A: фильтрация по «своему направлению» (например, только заказы конкретного продавца) — это уровень Customer BFF / Seller BFF, не Notification.
Нет ролей customer или seller: пользователи не имеют прямого доступа к Notification — свой inbox видят через Customer BFF, который запрашивает у Notification нужные строки от имени пользователя (REST + service-to-service auth).
6. Бизнес-правила
BR-N1 Идемпотентность по event_id. Повторная доставка одного и того же сообщения из Kafka не создаёт дубль уведомления. Реализуется через processed_events — INSERT ... ON CONFLICT DO NOTHING; если конфликт — событие уже обработано.
BR-N2 Каналы выбираются жёстко по типу события. Таблица в коде:
| Событие | PUSH | |
|---|---|---|
OrderConfirmed | ✅ | ✅ |
OrderPaid | ✅ | ✅ |
OrderShipped | ✅ (с трек-номером) | ✅ |
OrderDelivered | ✅ (запрос отзыва) | — |
OrderCancelled | ✅ | ✅ |
OrderRefunded | ✅ | ✅ |
DisputeOpened | — | ✅ продавцу + email оператору |
DisputeResolved | ✅ | ✅ |
«Перевести правило в БД» (динамический выбор каналов через админку) — расширение Tier B.
BR-N3 Шаблон обязателен. Если для пары (event_type, channel, locale) шаблон не найден — уведомление не создаётся (запись не появляется в notifications). Создаётся алерт в Prometheus (notification_template_missing_total). Это бизнес-инвариант: лучше пропустить уведомление, чем отправить пустое.
BR-N4 Контакт материализуется на момент отправки. Email и push-токены, полученные из Customer BFF, записываются в notifications.contact. Если потом пользователь поменяет email, в журнале остаётся тот, на который реально ушло письмо.
BR-N5 Retry-политика. 3 попытки с задержками 30s / 5min / 30min при TRANSIENT_ERROR (5xx от провайдера, network timeout). При PERMANENT_ERROR (4xx, например невалидный формат email) — сразу FAILED, без ретраев.
BR-N6 Webhook deduplication. SMTP-провайдер может прислать одно и то же delivered-событие дважды. Дедупликация — по (notification_id, webhook_event_id); повтор просто игнорируется.
BR-N7 PUSH без подтверждения доставки. FCM не возвращает webhook о доставке. Уведомление, ушедшее в FCM с HTTP 200, помечается SENT и никогда не переходит в DELIVERED. Это явное ограничение, которое надо понимать оператору при чтении журнала.
BR-N8 Срок хранения журнала — 90 дней. Старше 90 дней notifications и delivery_attempts удаляются ночным джобом. Юридическое обоснование: для прохождения GDPR/152-ФЗ нужно ограничить хранение логов с PII (email, push-токен).
BR-N9 Ограничение скорости отправки. Не более 100 писем в секунду в одного провайдера. Реализуется как in-memory token bucket — для Tier A этого хватает (один инстанс на старте, потом — Redis-based лимитер при горизонтальном масштабировании).
7. Операции (Commands)
На Tier A — просто список операций, не UseCase записи.
Внутренние (триггерятся событиями / шедулерами)
| Операция | Триггер | Что делает |
|---|---|---|
processOrderEvent(record) | Kafka-консьюмер marketplace.orders.v1 | Создаёт Notification-записи (по таблице каналов из BR-N2), отдаёт каждую в очередь отправки. Идемпотентен по event_id. |
dispatchPending() | каждые 1s, шедулер | Берёт из БД notifications со статусом QUEUED (limit 100), отправляет через провайдера, обновляет статус. |
processEmailWebhook(payload) | webhook от Mailgun | По external_id находит notification, переводит в DELIVERED или BOUNCED. |
purgeOldRecords() | ежедневно 03:00 | Удаляет записи старше 90 дней (BR-N8). |
Операторские (через REST)
| Операция | Эндпоинт | Что делает |
|---|---|---|
| Retry уведомления | POST /api/v1/notifications/{id}/retry | Только для FAILED. Сбрасывает last_error, ставит статус QUEUED, добавляет новую запись в delivery_attempts. |
| Пометить «безнадёжно» | POST /api/v1/notifications/{id}/abandon | Из FAILED уводит в финальный FAILED без возможности retry. Используется когда оператор убедился, что email невалидный. |
На Tier B эти операции стали бы парами
RetryNotificationUseCase/AbandonNotificationUseCase. На Tier A — методыNotificationService, ничего не теряем.
8. Queries / Read Model
Чтения, нужные оператору и Customer BFF.
| Запрос | Кому | Параметры | Результат |
|---|---|---|---|
| Список уведомлений с фильтром | оператор | userId?, status?, eventType?, channel?, from, to, пагинация | страница Notification с базовыми полями |
| Детали уведомления | оператор | notificationId | полная запись + список delivery_attempts |
| Inbox пользователя | Customer BFF (service-to-service) | userId, пагинация | страница уведомлений канала EMAIL+PUSH, отсортированных по дате |
| Метрики дашборда | оператор / SRE | from, to | агрегаты по статусам, по каналам, по типам событий |
Read Model на Tier A — это просто SELECT-запросы поверх той же таблицы notifications. Отдельная Read Model появляется на Tier B+ (например, материализованная view для метрик).
9. Use Cases
Главные сквозные сценарии. На Tier A это истории, не формальные UseCase-записи.
UC-N1: Подтверждение заказа → email + push покупателю
- Order Service публикует
OrderConfirmedвmarketplace.orders.v1. - Notification-консьюмер получает запись, проверяет
event_idчерезprocessed_events. - Из payload вытаскивает
customerId, обращается в Customer BFF:GET /api/v1/users/{id}/contact→{email, pushTokens, locale}. - По таблице каналов (BR-N2) для
OrderConfirmed→EMAIL+PUSH. Создаёт две записиnotificationsсо статусомQUEUED. - Шедулер
dispatchPendingзабирает обе записи в очередь отправки. - EMAIL: рендерит шаблон
order.confirmed.email(subject + body с${order_id},${customer_name}), отправляет в Mailgun →external_id. СтатусSENT. - PUSH: отправляет в FCM → 200. Статус
SENT(без webhook). - Через ~30 секунд Mailgun присылает webhook
delivered→ статус EMAIL переходит вDELIVERED.
UC-N2: Mailgun отвечает 503 → ретрай
- Notification отправляет email, Mailgun отвечает
503. - Создаётся запись в
delivery_attemptsсresult=TRANSIENT_ERROR. - Через 30 секунд (BR-N5) шедулер делает повтор. Mailgun снова
503. - Через 5 минут — третий повтор. Mailgun снова
503. - Все три попытки исчерпаны → статус
FAILED,last_error="Mailgun 503 после 3 попыток". Алертnotification_failed_total > 0стреляет в Prometheus. - Оператор открывает админку, видит
FAILED-уведомление, делаетPOST /retry. Статус →QUEUED, попадает в очередь, отправляется успешно.
UC-N3: Адрес невалидный → BOUNCED
- Notification отправляет email на
notexist@gmail.com, Mailgun принимает (200) → статусSENT. - Через 2 минуты Mailgun присылает webhook
bounced(категорияpermanent). - Notification переводит запись в
BOUNCED. - Оператор видит в админке причину: «Mailgun:
permanent bounce — invalid recipient».
UC-N4: Ручной разбор оператора
- Покупатель пишет в поддержку: «не пришёл код подтверждения».
- Оператор открывает админку, фильтрует по
userId, видит записьnotificationсо статусомBOUNCED. - Кликает «детали» — видит
delivery_attempts, ответ Mailgun: «adresat unreachable». - Сообщает покупателю в чате: «у вас невалидный email, обновите в профиле».
10. UI-спецификация
Один интерфейс — админка для support-operator. Никаких пользовательских интерфейсов сам Notification не имеет (inbox показывает Customer BFF).
Экран «Журнал уведомлений» (/admin/notifications)
| Элемент | Поведение |
|---|---|
| Фильтры (top bar) | userId (ввод), status (multi-select), eventType (multi-select), channel (toggle EMAIL/PUSH), диапазон дат |
| Таблица | колонки: Дата, Получатель (email/userId), Канал, Тип события, Статус (цветной badge), действия (detail, retry если FAILED) |
| Пагинация | offset/limit, дефолт 50 на страницу |
| Counters | в шапке: «Сегодня: ✓ 1234 ⏳ 12 ✕ 3» (DELIVERED / QUEUED / FAILED) |
Экран «Детали» (/admin/notifications/{id})
- Полные поля
notifications(включаяsource_event_payloadв виде JSON-pretty). - Список
delivery_attemptsхронологически. - Кнопки
Retry(если статусFAILED) иAbandon(если статусFAILED).
Тексты ошибок UI
| Сценарий | Что показываем |
|---|---|
| Retry успешен | toast «Уведомление поставлено в очередь» |
| Retry на не-FAILED | toast-error «Можно ретраить только FAILED-уведомления» (404 от backend, маппим) |
| Abandon | confirm-диалог: «Это финальное действие, уведомление больше не будет отправлено. Уверены?» |
11. Каталог ошибок
Notification — внутренний сервис, ошибки видит только оператор и интеграторы (Mailgun webhook, Customer BFF). Поэтому короткий каталог. Формат RFC 9457 Problem Details.
code | HTTP | Когда |
|---|---|---|
NOTIFICATION_NOT_FOUND | 404 | retry / детали по несуществующему id |
INVALID_STATUS_FOR_RETRY | 409 | retry на не-FAILED |
WEBHOOK_SIGNATURE_INVALID | 401 | webhook от Mailgun без правильной HMAC-подписи |
TEMPLATE_MISSING | — (внутренняя) | пишется в notification.last_error и уведомление не создаётся (BR-N3); внешне не видна |
CONTACT_LOOKUP_FAILED | — (внутренняя) | Customer BFF недоступен или 5xx; уведомление получает FAILED после ретраев |
PROVIDER_UNAVAILABLE | — (внутренняя) | Mailgun/FCM недоступен; уведомление получает FAILED после ретраев |
Внутренние ошибки наружу не светят — для интеграторов это либо 200 (webhook принят), либо 5xx (наша проблема).
12. Интеграции
Inbound: Kafka
| Топик | Событие | Действие |
|---|---|---|
marketplace.orders.v1 | OrderConfirmed / OrderPaid / OrderShipped / OrderDelivered / OrderCancelled / OrderRefunded / DisputeOpened / DisputeResolved | создать Notification по таблице каналов |
Контракт события — определяется Order Service. Notification — Conformist: подстраивается под чужой контракт, никогда не диктует. Это нормально для Tier A: писать своё anti-corruption layer над schema из Order Service оверкилл, проще закладываться на стабильность контракта (semver).
Inbound: REST (от SMTP-провайдера)
POST /webhooks/email-events
X-Mailgun-Signature: <hmac-sha256>
X-Mailgun-Timestamp: <unix-ts>
X-Mailgun-Token: <random>
{
"event": "delivered" | "bounced" | "complained",
"id": "abc-123",
"message": { "headers": { "message-id": "..." } },
"recipient": "user@example.com",
"timestamp": 1700000000
}
HMAC-валидация заголовка обязательна (BR — WEBHOOK_SIGNATURE_INVALID). Дедупликация — по (notification.external_id, webhook_event_id).
Inbound: REST (от Admin UI)
| Метод | Путь | Кто | Что |
|---|---|---|---|
GET | /api/v1/notifications | оператор | список с фильтрами (см. §8) |
GET | /api/v1/notifications/{id} | оператор | детали |
POST | /api/v1/notifications/{id}/retry | оператор | retry |
POST | /api/v1/notifications/{id}/abandon | оператор | финализация без retry |
Outbound: REST (Customer BFF)
GET /api/v1/users/{userId}/contact
→ {
"userId": "...",
"email": "user@example.com",
"pushTokens": ["...fcm-token-1...", "...fcm-token-2..."],
"locale": "ru"
}
Resilience: timeout 1s, retry 1, circuit breaker. При недоступности — уведомление помечается FAILED с last_error="contact lookup unavailable", оператор может ретраить когда Customer BFF поднимется.
Outbound: SMTP-провайдер
POST https://api.mailgun.net/v3/{domain}/messages
Authorization: Basic ...
Content-Type: multipart/form-data
from=<no-reply@marketplace.ru>
to=<user@example.com>
subject=Заказ ABC-123 подтверждён
html=<отрендеренное тело>
o:tracking=yes ← включает webhook 'delivered'/'bounced'
→ 200 { "id": "<external_id>", "message": "Queued. Thank you." }
external_id сохраняется в notification.external_id для матчинга с webhook'ами.
Outbound: FCM
POST https://fcm.googleapis.com/v1/projects/{project}/messages:send
Authorization: Bearer <oauth-token>
{ "message": { "token": "...", "notification": { ... } } }
→ 200
Без webhook — статус сразу SENT.
13. Критерии приёмки
Минимально приёмочная функциональность для запуска.
| AC | Описание |
|---|---|
| AC-N1 | На каждое из восьми событий Order'а (см. §6 BR-N2) создаются записи в правильных каналах. Тестами через embedded Kafka. |
| AC-N2 | Идемпотентный консьюмер: повторная доставка не создаёт второй notification (BR-N1). |
| AC-N3 | Mailgun-webhook delivered переводит SENT → DELIVERED. |
| AC-N4 | Mailgun-webhook bounced переводит SENT → BOUNCED. |
| AC-N5 | Невалидная HMAC-подпись webhook → 401. |
| AC-N6 | Транзиентная ошибка провайдера → 3 попытки, затем FAILED. |
| AC-N7 | Permanent error (4xx) — без ретраев, сразу FAILED. |
| AC-N8 | Шаблон отсутствует — уведомление не создаётся, метрика notification_template_missing_total++. |
| AC-N9 | POST /retry на FAILED — переводит в QUEUED, на не-FAILED — 409. |
| AC-N10 | Ночной purge удаляет записи старше 90 дней. |
| AC-N11 | Журнал доставки оператор видит с фильтрами и пагинацией. |
| AC-N12 | Customer BFF недоступен → уведомление в FAILED, retry оператора срабатывает после восстановления BFF. |
14. Нефункциональные требования
Производительность
- Пиковая нагрузка: 500 событий Order'а в секунду (по верхней границе планов маркетплейса) → до 1000 уведомлений (email + push). Один инстанс держит, без шардирования.
- p95 latency «событие → email ушёл в Mailgun» — ≤ 5 секунд.
- p99 — ≤ 30 секунд (включая 1 retry при transient).
Надёжность
- At-least-once доставка: лучше отправить дважды (получатель увидит дубль), чем не отправить вообще. Идемпотентность на уровне консьюмера (BR-N1) защищает от типичных дублей; редкие edge-кейсы вроде сбоя между «отправили» и «commit offset» допускаем — это менее критично, чем потерять подтверждение заказа.
- При сбое инстанса в момент
dispatchPending— потерянныеQUEUED-уведомления подберутся следующим запуском. Идемпотентности на провайдере нет, поэтому возможен дубль письма (см. ниже про MailgunIdempotency-Key).
Безопасность
- HMAC-валидация всех webhook'ов от Mailgun. Подпись проверяем по
X-Mailgun-Signature+X-Mailgun-Timestamp(timestamp защищает от replay > 5 минут). - PII (email, push-token) шифруется в БД на уровне Postgres TDE. В логах — маскируется (
u***@example.com). - 90-дневный TTL (BR-N8) — обязательный compliance-фактор, не «приятная фича».
- Service-to-service вызовы Customer BFF — через JWT с
service-accountролью, не от имени пользователя.
Наблюдаемость
- Метрики Prometheus:
notification_created_total{event_type, channel},notification_status_total{status},notification_provider_latency_seconds{provider},notification_template_missing_total,notification_retry_total{reason}. - Алёрты:
notification_failed_total > 5/min— что-то системно сломалось.notification_template_missing_total > 0— где-то пропустили шаблон, добавлен новый event_type.notification_provider_latency_seconds{p99} > 10s— провайдер тормозит.
- Логи:
event_id,notification_id,external_idпишутся в каждой строке для трейсинга. Tracing — OpenTelemetry, span-ы для consume → format → send.
Эксплуатация
- Обновление шаблона — без рестарта (
UPDATE templates SET body = ... WHERE key = ...). Кэшировать шаблоны нельзя или с TTL ≤ 60 секунд. - Подключение нового провайдера — отдельный adapter. На запуске — Mailgun + FCM, прозрачно расширяется до SES + APNs.
- Локализация — поле
localeу пользователя (берём в Customer BFF). На запуске два языка:ru,en. Шаблонов — 8 событий × 2 канала × 2 языка = 32 строки вtemplates.
15. Стек технологий
Java 21
Spring Boot 3.4.x
Spring Kafka — консьюмер marketplace.orders.v1
Spring Web — REST для webhooks и admin API
Spring Security — OAuth2 Resource Server + HMAC-фильтр
spring-boot-starter-jooq — слой доступа к БД (см. ниже)
PostgreSQL 16 — notifications, templates, delivery_attempts, processed_events
+ ENUM-типы notification_channel/notification_status/delivery_attempt_result
Liquibase — схема и шаблоны как ChangeSet
nu.studer.jooq 10.x — кодогенерация POJO/Records/Tables/Enums из applied-схемы
Mailgun (REST) — outbound email
Firebase Cloud Messaging (REST)— outbound push
Resilience4j — circuit breaker / retry для Customer BFF
Micrometer + Prometheus — метрики
OpenTelemetry — distributed tracing
JUnit 5 + WireMock — интеграционные тесты с реальным Postgres и WireMock-стабами
Почему jOOQ, и почему только generated классы
jOOQ — единый persistence-стандарт во всех сервисах команды, независимо от Tier'а. Ни JdbcTemplate, ни JPA, ни MyBatis не используем — это командное правило, фиксируемое одной строкой BS-17 из spring-bootstrap-style-guide.
Только сгенерированные классы. Цель — меньше кода и один источник правды (Liquibase-схема → jOOQ codegen → Java). Вместо handcrafted Notification.java, дублирующего строку БД, используем сгенеренный NotificationsPojo. Вместо Channel.java/NotificationStatus.java — generated-enum'ы из generated.enums, основанные на Postgres enum types в схеме.
Если на enum нужны методы (например, isTerminal()/canRetry()) — inline'им проверку на use-sites или кладём в utility-класс. Generated-классы не модифицируем — это нарушает «один источник правды».
Tier A не означает «упростить persistence». Tier A — про отсутствие DDD-агрегатов и UseCase Pattern. Persistence остаётся на jOOQ, как и в Tier C.
Расширения, которые не делаем в первой версии (Tier B/C)
- Подписки и опт-ауты — потребует таблицы
user_preferencesи UseCase'ыUpdatePreferences/Unsubscribe. Это уже честный Tier B. - A/B-тесты текстов — требует разделения
experiment_variantвnotifications, отдельной аналитики. Tier B+. - Push на Web (WebPush) — отдельный провайдер, не критично для запуска.
- Кампании / массовые рассылки — отдельный сервис, не Notification. Notification живёт только на транзакционных событиях.
- Динамический выбор каналов через админку — таблица
channel_rulesс приоритетами; Tier B.
Полная машинно-читаемая копия этой спеки — в github.com/remodov/notification-service/docs/spec/ вместе с кодом. Каждый раздел — отдельный .md-файл с YAML-фронтматтером, который скиллы методологии читают для генерации кода: контроллеры, jOOQ-репозитории, шедулеры, тесты.