Patterns
Архитектурные паттерны, которые применяются в наших сервисах. Каждый — deep-dive: не только «как», но и «почему», «какие trade-off’ы», «когда не использовать».
Reference-правила про Watermill, Kafka и HTTP — в
../conventions/. Страницы здесь отвечают на
вопрос «какой архитектурный приём применить для этой задачи».
Страницы
outbox— Transactional Outbox. Публикация в Kafka идёт в две стадии: запись в таблицу outbox внутри бизнес- транзакции + асинхронный forwarder. Убирает dual-write.saga— Saga. Координированная запись в N сервисов с компенсациями. Orchestration-style поверх outbox + Kafka, state вsaga_instances.cqrs— CQRS черезwatermill/components/cqrs. Типизированные Command/Event handler’ы вместоswitchпоEvent-Type. Lite-вариант, без отдельных read-моделей.idempotent-consumer— Idempotent Consumer. Защита от at-least-once дублей Kafka:middleware.Deduplicatorповерх Redis + идемпотентность на БД-уровне.api-composition— API Composition. Склейка данных из нескольких сервисов для list-view; без shared database и cross-service JOIN. Timeouts, partial failure, N+1.retry-and-circuit-breaker— Retry + Circuit Breaker. Как повторять запросы к downstream и когда «размыкать цепь», чтобы не добивать уже лежащий сервис. Exponential backoff + jitter, идемпотентность как условие, fallback стратегии.
Когда использовать какой
| Задача | Паттерн |
|---|---|
| Публикация события в Kafka из бизнес-операции | outbox |
| Consumer принимает событие и меняет БД | idempotent-consumer (вместе с outbox на publisher-стороне) |
| Координированная запись в N сервисов с откатом | saga |
| В сервисе > 3 команд/событий на одну доменную модель | cqrs |
| Нужно отдать UI-клиенту данные из 2+ сервисов одним ответом | api-composition |
| Внешний сервис / БД / Redis периодически падает, нужна защита от transient-ошибок и каскадного падения | retry-and-circuit-breaker |
Как паттерны связаны между собой
Outbox и idempotent consumer — инфраструктурные паттерны, поверх которых работают saga и CQRS. Любая асинхронная коммуникация между сервисами проходит через outbox на publisher-стороне и Deduplicator на consumer-стороне. Saga использует те же каналы для команд и событий шагов; CQRS-проекции подписываются на те же topic’и.
Outbox отвечает за «запись в БД и публикация события — атомарны»;
idempotent consumer — за «повторная доставка не вызывает повторного
эффекта». Saga поверх этих двух паттернов строит координацию: команды
шагам она шлёт через outbox (то есть в той же транзакции, что и запись
в saga_instances), ответы шагов читает через обычный consumer со
стандартным Deduplicator’ом. CQRS-проекция поверх них же делает read-
модель в своей БД.
Decision tree: какой паттерн применить
- Нужно опубликовать событие в том же tx, что бизнес-операция → outbox.
- Consumer может получить сообщение дважды (at-least-once от Kafka) → idempotent-consumer.
- Нужна read-модель с денормализацией по событиям из нескольких сервисов → cqrs (через projection).
- Нужно собрать данные из N сервисов для одного API-ответа (read-only composition) → api-composition.
- Нужна координированная запись в N сервисов с откатом (мутации с компенсациями) → saga.
- Нестабильный downstream (HTTP к чужому сервису / Redis / внешний API) — нужна защита от transient-ошибок и каскадного падения → retry-and-circuit-breaker.
Если задача не попадает ни в один пункт — скорее всего, паттерн здесь не нужен. Не подключай CQRS «на вырост», не заводи outbox без Kafka-события, не оборачивай всё в сагу, если на самом деле изменение локально в одном сервисе.