Серия разобрала паттерны вокруг событий, состояния и долгих процессов — от того, что положить в событие, до durable-оркестрации. Главный риск после такого чтения — соблазн применить всё сразу: «возьмём event sourcing, сверху CQRS, рядом saga, оркестрируем Temporal». Так рождаются системы, которые невозможно ни понять, ни эксплуатировать.
Эта статья — навигатор. Она не вводит новых паттернов, а помогает выбрать минимально достаточный для конкретной задачи. Главный принцип всей серии: каждый паттерн — это ответ на конкретную боль, а не улучшение по умолчанию. Нет боли — нет паттерна.
В статье
- Четыре оси выбора
- Матрица: задача → паттерн
- Маршрут принятия решения
- Как паттерны комбинируются
- Антипаттерны комбинаций
- Сквозная наблюдаемость
- Главное правило
Четыре оси выбора
Почти любой выбор в этой области раскладывается на четыре вопроса:
- Границы — процесс живёт в одном сервисе/транзакции или пересекает несколько хранилищ?
- История — нужна ли полная история изменений как источник правды, или достаточно текущего состояния (возможно, с audit log)?
- Длительность — процесс завершается за миллисекунды, или длится минуты, часы, дни (ожидание, согласование)?
- Согласованность — допустимо ли отставание read-стороны (eventual consistency) или нужна строгая согласованность на чтение?
Ответы на эти четыре вопроса почти однозначно указывают на нужный паттерн — или на его отсутствие.
Матрица: задача → паттерн
| Задача / сигнал | Решение | Почему |
|---|---|---|
| Текущего состояния достаточно, история «на всякий случай» | CRUD + audit log | Паттерны не нужны — это базовый случай |
| Меняем данные и публикуем событие об этом | Outbox/Inbox | Надёжная публикация без распределённой транзакции |
| Событие может прийти дважды (at-least-once, ретраи) | идемпотентный консьюмер | Базовый минимум: дубли гасим на потребителе (effectively-once) |
| Класть в событие ID или полное состояние? | notification vs state transfer | Связанность ↔ автономность ↔ staleness |
| Чтений сильно больше записей / нужны разные представления | CQRS | Раздельная оптимизация чтения и записи |
| История изменений — источник правды (аудит, temporal queries) | Event Sourcing | Состояние как последовательность событий |
| Транзакция пересекает несколько сервисов с разными БД | Saga | Локальные транзакции + компенсации вместо 2PC |
| Многошаговый процесс: центральный дирижёр или реакция на события? | хореография vs оркестрация | Где живёт знание о процессе |
| Многошаговый длинный процесс с ретраями, таймаутами, ожиданием | Temporal | Durable workflow вместо самодельного state machine |
| Несколько сервисов обмениваются событиями | контракты + Outbox | Совместимость схем и надёжная доставка |
Маршрут принятия решения
Если нужен пошаговый маршрут, а не таблица:
- Хватает ли текущего состояния? Да и история не критична → CRUD (+ audit log при необходимости). Стоп.
- Публикуете ли события наружу? Да → нужен Outbox/Inbox и продуманные контракты. Это база межсервисного обмена. Сразу два базовых решения: что кладём в событие (уведомление или состояние) и идемпотентность консьюмера — доставка at-least-once, дубли неизбежны.
- История — источник правды? (аудит, compliance, temporal queries, replay) Да → Event Sourcing. Нет → обычные таблицы.
- Асимметрия чтения/записи или много представлений? Да → CQRS (начиная с лёгкого разделения интерфейсов).
- Процесс пересекает границы сервисов? Да → Saga (с компенсациями и идемпотентностью). И сразу — кто дирижирует: реакция на события (хореография) или центральный координатор (оркестрация).
- Процесс длинный, многошаговый, с ожиданием и человеком в цикле? Да, и самодельный state machine уже болит → Temporal.
Заметьте: пункты 1–2 покрывают большинство систем (и уже требуют выбора payload и идемпотентности). Паттерны 3–6 включаются только при явных сигналах.
Как паттерны комбинируются
Паттерны не взаимоисключающие — они складываются слоями:
- Outbox — почти универсальная основа, как только есть межсервисные события. Под Saga, под интеграцию, под проекции CQRS.
- Event Sourcing + CQRS — естественная связка: события это write-сторона, проекции — read-сторона.
- Saga + Outbox — Saga публикует свои шаги-события через outbox, иначе теряет надёжность.
- Temporal вместо оркестрационной Saga — когда оркестратор саги дорос до полноценного workflow с таймаутами и долгим ожиданием, Temporal заменяет самодельную координацию.
Здоровая система чаще выглядит как «CRUD + outbox для большинства сервисов, и точечно event sourcing / saga / temporal там, где есть конкретная боль», а не как «всё везде».
Антипаттерны комбинаций
Сочетания, которые сигналят о переусложнении:
- Event Sourcing для CRUD-домена — справочники и настройки в виде потока событий: вся сложность, ноль пользы.
- CQRS уровня 3 без метрик — раздельные хранилища с eventual consistency там, где нет ни асимметрии нагрузки, ни требования к разным представлениям.
- Saga там, где транзакция помещается в одну БД — компенсации и идемпотентность ради процесса, который мог быть
BEGIN ... COMMIT. - Temporal ради fire-and-forget задач — durable workflow engine для одношаговых воркеров.
- Event sourcing без стратегии версионирования и забвения — технический долг, который выстрелит через год (см. статьи про event sourcing и контракты).
Общий признак антипаттерна: паттерн внедрён «потому что правильно/современно», а не в ответ на измеренную проблему.
Сквозная наблюдаемость
Какой бы набор паттернов вы ни выбрали, событийная система непрозрачна по своей природе: связи развязаны, эффекты отложены, сбои тихие. Поэтому наблюдаемость — не опция, а условие эксплуатации. Минимальная карта метрик event-driven системы:
| Метрика | Сигнализирует о |
|---|---|
| Outbox lag — возраст старейшей неопубликованной записи | relay не справляется или упал |
| Consumer lag — отставание потребителя от потока | обработка медленнее публикации |
| DLQ size — размер dead-letter queue | накопление poison messages |
| Retry rate — частота повторов | деградация downstream-сервиса |
| Duplicate rate — доля отброшенных дублей | проблемы доставки или сбои relay |
| Schema validation failures — отказы валидации схемы | несовместимое изменение контракта |
| Stuck saga / workflow count — зависшие процессы | застрявшие компенсации или таймауты |
Эти метрики не привязаны к одному паттерну — они складываются по мере того, как вы вводите Outbox, контракты, Saga, Temporal. Заводить их стоит вместе с самим паттерном, а не после первого инцидента: задним числом восстановить, что и когда пошло не так в развязанной системе, почти невозможно. Связку метрик с трассировкой через correlation/causation id серия уже обсуждала в статьях про Outbox и контракты.
Главное правило
Если из всей серии стоит запомнить одну мысль — вот она:
Начинайте с самого простого, что решает задачу сегодня. Усложняйте только когда появляется измеримая причина — метрика, инцидент, конкретное требование. Каждый паттерн платит за себя сложностью эксплуатации, и этот счёт приходит не в день внедрения, а через полгода.
Событийная архитектура — мощный набор инструментов. Сила приходит не от того, сколько паттернов вы применили, а от того, насколько точно каждый из них соответствует реальной боли. Эта серия — про то, чтобы выбирать осознанно, а не по моде.
С этого навигатора удобно возвращаться к любому выпуску серии — начните с карты понятий, если нужна общая рамка.
Документация и первоисточники
- Канон: Martin Fowler — Event-Driven, microservices.io — data patterns.
- Все паттерны серии: карта, что кладём в событие, гарантии доставки и идемпотентность, Outbox, контракты событий, Event Sourcing, CQRS, Saga, хореография vs оркестрация, Temporal.
- Практический блок: Event Sourcing на практике, CQRS на практике, Saga на практике.
Комментарии