Notification Service — Use Case спецификация (Tier A)

Полная Use Case спецификация Notification Service из кейса маркетплейса. Tier A: legacy / слоёная архитектура без UseCase Pattern и DDD. Контрпример к Order Service — показывает, когда DDD overkill.

Эталонный пример реализации notification-service (Spring Boot + JdbcTemplate) Notification Service спецификация

Эта спека — контрпример к 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, из которой скиллы методологии генерируют код.

Содержание

  1. Контекст / модуль
  2. Ubiquitous Language
  3. Domain Model
  4. Жизненный цикл уведомления
  5. Роли и права
  6. Бизнес-правила
  7. Операции (Commands)
  8. Queries / Read Model
  9. Use Cases
  10. UI-спецификация
  11. Каталог ошибок
  12. Интеграции
  13. Критерии приёмки
  14. Нефункциональные требования
  15. Стек технологий

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 Serviceinboundподписка на топик marketplace.orders.v1 (Kafka)
Customer BFFoutbound RESTGET /api/v1/users/{id}/contact — email + push-токены пользователя
SMTP-провайдер (Mailgun/SendGrid)outbound + inbound webhookPOST /messages для отправки; webhook /notifications/email-events для bounce/delivered
Firebase Cloud MessagingoutboundPOST /fcm/send — fire-and-forget, без webhook
Admin UI / операторinbound RESTGET /api/v1/notifications (фильтрация), POST /retry

Стейкхолдеры

  • Владелец: команда «Платформа» (Platform team) — Notification считается инфраструктурным сервисом.
  • Зависят от нас: Customer BFF (показывает inbox), оператор поддержки (читает журнал доставок при разборе жалоб).
  • От кого зависим: Order Service (источник событий), Customer BFF (контакты), SMTP-провайдер, FCM.

Диаграмма C1

diagram

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-схема

diagram

Таблицы — словами

  • 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. Жизненный цикл уведомления

diagram

Терминальные состояния: DELIVERED, BOUNCED, и FAILED после ручной отметки оператора.

Замечания:

  • PUSH-канал не имеет webhook'а — статус автоматически становится SENT и остаётся таким; DELIVERED/BOUNCED для push не достижимы. Это нормальное ограничение FCM.
  • Между QUEUED и SENT уведомление может несколько раз пройти через delivery_attempts с result=TRANSIENT_ERROR. Видимый статус остаётся QUEUED до первой удачной отправки.
  • Ручной retry возможен только для FAILED. Из BOUNCED retry бесполезен — провайдер сказал «адрес недоставляем».

5. Роли и права

Notification — внутренний сервис, у него нет публичного API для покупателей. Все REST-эндпоинты делятся на две группы:

EndpointКому
POST /webhooks/email-eventsSMTP-провайдер (фиксированный 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_eventsINSERT ... ON CONFLICT DO NOTHING; если конфликт — событие уже обработано.

BR-N2 Каналы выбираются жёстко по типу события. Таблица в коде:

СобытиеEMAILPUSH
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, отсортированных по дате
Метрики дашбордаоператор / SREfrom, toагрегаты по статусам, по каналам, по типам событий

Read Model на Tier A — это просто SELECT-запросы поверх той же таблицы notifications. Отдельная Read Model появляется на Tier B+ (например, материализованная view для метрик).

9. Use Cases

Главные сквозные сценарии. На Tier A это истории, не формальные UseCase-записи.

UC-N1: Подтверждение заказа → email + push покупателю

  1. Order Service публикует OrderConfirmed в marketplace.orders.v1.
  2. Notification-консьюмер получает запись, проверяет event_id через processed_events.
  3. Из payload вытаскивает customerId, обращается в Customer BFF: GET /api/v1/users/{id}/contact{email, pushTokens, locale}.
  4. По таблице каналов (BR-N2) для OrderConfirmedEMAIL + PUSH. Создаёт две записи notifications со статусом QUEUED.
  5. Шедулер dispatchPending забирает обе записи в очередь отправки.
  6. EMAIL: рендерит шаблон order.confirmed.email (subject + body с ${order_id}, ${customer_name}), отправляет в Mailgun → external_id. Статус SENT.
  7. PUSH: отправляет в FCM → 200. Статус SENT (без webhook).
  8. Через ~30 секунд Mailgun присылает webhook delivered → статус EMAIL переходит в DELIVERED.

UC-N2: Mailgun отвечает 503 → ретрай

  1. Notification отправляет email, Mailgun отвечает 503.
  2. Создаётся запись в delivery_attempts с result=TRANSIENT_ERROR.
  3. Через 30 секунд (BR-N5) шедулер делает повтор. Mailgun снова 503.
  4. Через 5 минут — третий повтор. Mailgun снова 503.
  5. Все три попытки исчерпаны → статус FAILED, last_error="Mailgun 503 после 3 попыток". Алерт notification_failed_total > 0 стреляет в Prometheus.
  6. Оператор открывает админку, видит FAILED-уведомление, делает POST /retry. Статус → QUEUED, попадает в очередь, отправляется успешно.

UC-N3: Адрес невалидный → BOUNCED

  1. Notification отправляет email на notexist@gmail.com, Mailgun принимает (200) → статус SENT.
  2. Через 2 минуты Mailgun присылает webhook bounced (категория permanent).
  3. Notification переводит запись в BOUNCED.
  4. Оператор видит в админке причину: «Mailgun: permanent bounce — invalid recipient».

UC-N4: Ручной разбор оператора

  1. Покупатель пишет в поддержку: «не пришёл код подтверждения».
  2. Оператор открывает админку, фильтрует по userId, видит запись notification со статусом BOUNCED.
  3. Кликает «детали» — видит delivery_attempts, ответ Mailgun: «adresat unreachable».
  4. Сообщает покупателю в чате: «у вас невалидный 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 на не-FAILEDtoast-error «Можно ретраить только FAILED-уведомления» (404 от backend, маппим)
Abandonconfirm-диалог: «Это финальное действие, уведомление больше не будет отправлено. Уверены?»

11. Каталог ошибок

Notification — внутренний сервис, ошибки видит только оператор и интеграторы (Mailgun webhook, Customer BFF). Поэтому короткий каталог. Формат RFC 9457 Problem Details.

codeHTTPКогда
NOTIFICATION_NOT_FOUND404retry / детали по несуществующему id
INVALID_STATUS_FOR_RETRY409retry на не-FAILED
WEBHOOK_SIGNATURE_INVALID401webhook от 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.v1OrderConfirmed / 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-N3Mailgun-webhook delivered переводит SENTDELIVERED.
AC-N4Mailgun-webhook bounced переводит SENTBOUNCED.
AC-N5Невалидная HMAC-подпись webhook → 401.
AC-N6Транзиентная ошибка провайдера → 3 попытки, затем FAILED.
AC-N7Permanent error (4xx) — без ретраев, сразу FAILED.
AC-N8Шаблон отсутствует — уведомление не создаётся, метрика notification_template_missing_total++.
AC-N9POST /retry на FAILED — переводит в QUEUED, на не-FAILED — 409.
AC-N10Ночной purge удаляет записи старше 90 дней.
AC-N11Журнал доставки оператор видит с фильтрами и пагинацией.
AC-N12Customer 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-уведомления подберутся следующим запуском. Идемпотентности на провайдере нет, поэтому возможен дубль письма (см. ниже про Mailgun Idempotency-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-репозитории, шедулеры, тесты.