Что выбрать: матрица решений по событийным паттернам

Финальная карта серии: когда хватит CRUD с audit log, когда нужен Outbox, CQRS, Event Sourcing, Saga или Temporal — по осям границ, истории, длительности процесса и требований к согласованности

Серия разобрала паттерны вокруг событий, состояния и долгих процессов — от того, что положить в событие, до durable-оркестрации. Главный риск после такого чтения — соблазн применить всё сразу: «возьмём event sourcing, сверху CQRS, рядом saga, оркестрируем Temporal». Так рождаются системы, которые невозможно ни понять, ни эксплуатировать.

Эта статья — навигатор. Она не вводит новых паттернов, а помогает выбрать минимально достаточный для конкретной задачи. Главный принцип всей серии: каждый паттерн — это ответ на конкретную боль, а не улучшение по умолчанию. Нет боли — нет паттерна.

В статье

Четыре оси выбора

Почти любой выбор в этой области раскладывается на четыре вопроса:

  1. Границы — процесс живёт в одном сервисе/транзакции или пересекает несколько хранилищ?
  2. История — нужна ли полная история изменений как источник правды, или достаточно текущего состояния (возможно, с audit log)?
  3. Длительность — процесс завершается за миллисекунды, или длится минуты, часы, дни (ожидание, согласование)?
  4. Согласованность — допустимо ли отставание 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 Совместимость схем и надёжная доставка

Маршрут принятия решения

Если нужен пошаговый маршрут, а не таблица:

  1. Хватает ли текущего состояния? Да и история не критична → CRUD (+ audit log при необходимости). Стоп.
  2. Публикуете ли события наружу? Да → нужен Outbox/Inbox и продуманные контракты. Это база межсервисного обмена. Сразу два базовых решения: что кладём в событие (уведомление или состояние) и идемпотентность консьюмера — доставка at-least-once, дубли неизбежны.
  3. История — источник правды? (аудит, compliance, temporal queries, replay) Да → Event Sourcing. Нет → обычные таблицы.
  4. Асимметрия чтения/записи или много представлений? Да → CQRS (начиная с лёгкого разделения интерфейсов).
  5. Процесс пересекает границы сервисов? Да → Saga (с компенсациями и идемпотентностью). И сразу — кто дирижирует: реакция на события (хореография) или центральный координатор (оркестрация).
  6. Процесс длинный, многошаговый, с ожиданием и человеком в цикле? Да, и самодельный 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 и контракты.

Главное правило

Если из всей серии стоит запомнить одну мысль — вот она:

Начинайте с самого простого, что решает задачу сегодня. Усложняйте только когда появляется измеримая причина — метрика, инцидент, конкретное требование. Каждый паттерн платит за себя сложностью эксплуатации, и этот счёт приходит не в день внедрения, а через полгода.

Событийная архитектура — мощный набор инструментов. Сила приходит не от того, сколько паттернов вы применили, а от того, насколько точно каждый из них соответствует реальной боли. Эта серия — про то, чтобы выбирать осознанно, а не по моде.

С этого навигатора удобно возвращаться к любому выпуску серии — начните с карты понятий, если нужна общая рамка.

Документация и первоисточники

Обсуждение в Telegram

Присоединиться →

Комментарии