Ловушки
Неизменяемость и eventual consistency event sourcing — возможности на бумаге и ограничения в продакшне: эволюция схемы, GDPR-удаление, плохие события, лаг проекций — всё требует осознанных структурных ответов. Знать, когда не применять ES, так же важно, как знать, как.
Через восемнадцать месяцев после запуска event-sourced биллинговой платформы одновременно горели три пожара. Первый: клиент подал запрос на удаление данных по GDPR. Юристы сказали, что имя, email и ИНН клиента нужно очистить в течение 30 дней. Юридический отдел предполагал, что инженеры смогут просто удалить нужные строки. Инженеры объяснили: персональные данные клиента встроены в сотни событий — OrderPlaced, InvoiceGenerated, PaymentReceived — разбросанных по неизменяемому журналу событий. Их удаление повредит журнал. Второй: инженер биллинга случайно добавил событие OrderApproved для неправильного ID заказа — ошибка из-за опечатки во время ручной операции поддержки. «Просто удалите его», — сказал менеджер поддержки. Инженеры объяснили снова: события в журнале неизменяемы. Удаление повредит replay для всех read models. Третий: projection финансов показывала устаревшие итоги примерно две секунды после каждого обновления заказа — обычно нормально — но в период высокой нагрузки pipeline projection переполнился, и лаг вырос до сорока секунд. Финансовый директор увидел дашборд с неправильными суммами просрочки и позвонил CEO. Три пожара, одна причина: команда приняла event sourcing без полного понимания его структурных ограничений.
Ловушка 1: eventual consistency между журналом и проекциями
В event-sourced системе журнал событий обновляется синхронно с каждой командой. Read models (проекции) обновляются асинхронно — они потребляют события из журнала и обновляют свои материализованные представления в отдельном процессе. Это создаёт окно устаревания: между добавлением события и моментом, когда projection его обработала и обновила read model, запросы возвращают предыдущее состояние.
При нормальной работе этот лаг — миллисекунды до единиц секунд. При высокой нагрузке, переполнении projection, или после перезапуска он может растянуться. Дашборд с неправильными суммами просрочки не был багом — это лаг projection, проявившийся под нагрузкой.
Управление лагом projection требует осознанного проектирования:
- Мониторить лаг как метрику первого класса: предупреждать, когда смещение projection-журнал превышает порог (секунды, а не минуты).
- Масштабировать воркеры projection на пиковую пропускную способность событий, а не среднюю.
- Проектировать read models с учётом устаревания: дашборд с пометкой «по состоянию на N секунд назад» лучше дашборда, молча показывающего неверные данные.
- Не использовать async projections для решений на write-пути: если write-сторона должна прочитать значение перед добавлением события, запрашивать состояние агрегата write-стороны, а не потенциально устаревшую read model.
Ловушка eventual consistency не уникальна для event sourcing — она появляется в любой CQRS-системе с async projection (юнит 07). В event sourcing она усилена, потому что read model — это единственный путь запросов; нормализованной таблицы текущего состояния, к которой можно обратиться, нет.
Ловушка 2: эволюция схемы — неизменяемое прошлое, меняющееся будущее
События — неизменяемые факты о прошлом. Доменная модель не статична. Эти две истины создают постоянное противоречие: структура события, корректная в 2023 году, может не соответствовать тому, что ожидает доменная модель в 2025-м.
Урок 2 ввёл upcasting как механизм. Ловушка — в операционной дисциплине, необходимой для работы в масштабе:
- Каждый тип события нуждается в идентификаторе версии (явном или встроенном в имя типа:
OrderApproved.v1,OrderApproved.v2). - Каждая функция upcast должна поддерживаться бессрочно — пока v1 события существуют в журнале, upcast v1 → v2 должен быть задеплоен.
- Аддитивные изменения (новые необязательные поля) безопасны: tolerant readers обрабатывают их значениями по умолчанию.
- Деструктивные изменения (переименование или удаление поля, изменение значения поля) требуют новой версии и стратегии миграции.
- Переименованное поле, присутствующее в тысячах проекций, создаёт широкий радиус поражения — каждую projection, читающую старое имя поля, нужно обновить.
Ловушка: команды, не enforcing версионирование событий с первого дня, накапливают неявные v1 события повсюду, а затем сталкиваются с болезненным распутыванием при первом breaking change. Дисциплина в именовании (OrderApproved.v1) и поддержке реестра функций upcast — это операционные накладные расходы, которые нужно планировать.
▸Why this works
Структурный урок: схема события — это публичный API с наихудшей политикой депрекации: нельзя удалять старых потребителей (журнал событий содержит их навсегда). К изменениям схемы событий нужно относиться с большей осторожностью, чем к изменениям REST API. Журнал событий — единственная зависимость, которую нельзя сломать.
Ловушка 3: GDPR и право на забвение
Статья 17 GDPR требует удаления персональных данных по запросу. Неизменяемый журнал событий — структурное препятствие: персональные данные, встроенные в payload событий, нельзя удалить, не повредив журнал.
Стандартный структурный ответ — crypto-shredding (также называется crypto-erasure):
- Персональные данные в payload событий шифруются ключом шифрования, специфичным для субъекта (один ключ на клиента или на границу субъекта данных).
- Журнал событий хранит зашифрованные персональные данные — шифротекст, а не открытый текст.
- При поступлении запроса на удаление ключ шифрования уничтожается. Payload событий в журнале по-прежнему существует, но персональные данные теперь вычислительно невосстановимы — фактически удалены.
Crypto-shredding имеет структурные предпосылки:
- Персональные данные должны быть идентифицированы заранее в каждом типе событий. Поля, добавленные спустя месяцы, могут быть пропущены.
- Key store должен быть отдельно от журнала событий. Потеря key store до обработки запроса на удаление означает, что данные никогда нельзя будет удалить.
- Проекции, материализовавшие открытый текст, тоже нужно обновить — crypto-shredding журнала событий автоматически не очищает read model, уже извлёкшую и сохранившую персональные данные в открытом тексте.
Команды, не проектирующие crypto-shredding с самого начала, сталкиваются с болезненным рефакторингом: идентификация всех персональных данных в сотнях типов событий, ретроактивное добавление шифрования (невозможно для уже хранящихся событий в открытом тексте) и потенциальное признание, что pre-retrofit персональные данные нельзя удалить из журнала.
Ловушка 4: плохое событие вырублено в камне
Во время ручной операции поддержки инженер добавил событие OrderApproved для неправильного ID заказа. В CRUD-системе исправление — UPDATE. В event-sourced системе UPDATE не существует.
Правильный ответ на плохое событие — compensating event (компенсирующее событие): новое событие, добавленное в журнал, отменяющее бизнес-эффект ошибочного. Для неправильного одобрения заказа добавляется событие OrderApprovalReverted с причиной и объяснением ошибки. Логика свёртки агрегата обрабатывает OrderApprovalReverted, возвращая заказ в предыдущее состояние.
Это не обходное решение — это предусмотренный механизм. Он сохраняет полный трейл аудита: одобрение, ошибка и исправление — всё видно в журнале. Аудитор комплаенса может видеть точно, что произошло, когда и почему.
▸lesson.inset.note
Compensating events требуют предусмотрительности: доменная модель должна определять события коррекции для каждой записи, которая может пойти не так операционно. Команды, обнаруживающие это только после того, как плохое событие уже добавлено, оказываются в худший возможный момент для проектирования схемы compensating event.
Структурное следствие: инструментарий операторов, пишущий напрямую в журнал событий, должен обрабатываться с той же дисциплиной, что и продакшн-код приложения. Добавление неправильного события через скрипт технического обслуживания вызывает ровно ту же проблему, что и баг в коде. Журналы событий в продакшне должны быть защищены контролем доступа с той же строгостью, что и продакшн-базы данных.
Биллинговая платформа получает запрос на удаление данных по GDPR для клиента #C-8821. Данные клиента (имя, email, ИНН) присутствуют в 340 событиях типов OrderPlaced, InvoiceGenerated и PaymentReceived. Система НЕ была спроектирована с crypto-shredding. Каковы реалистичные варианты и какова цена каждого?
Ловушка 5: операционная и сложностная стоимость — когда не применять event sourcing
Event sourcing — не дефолтный архитектурный выбор. Он несёт издержки, которые должны быть оправданы требованиями:
- Сложность write-стороны: вместо
INSERT/UPDATEкаждая запись требует загрузки агрегата (fold или снапшот + хвост), валидации команды, создания события и его атомарного добавления. Это значительно больше кода, чем CRUD. - Сложность read-стороны: нет нормализованной таблицы текущего состояния для запросов. Каждый запрос требует read model, которая требует projection, которая требует pipeline. Для системы с 20 типами сущностей это означает 20+ проекций для построения и поддержки.
- Операционная сложность: event store, воркеры projection, хранилище снапшотов и key store — всё это продакшн-инфраструктура. Каждый компонент нуждается в мониторинге, планировании ёмкости и восстановлении после сбоев. Лаг projection нужно мониторить как метрику первого класса.
- Сложность тестирования: event-sourced агрегаты тестируются утверждением об испускаемых событиях (дано эти прошлые события → когда эта команда → ожидаем эти новые события). Это освоимо, но незнакомо большинству backend-команд.
Инженер предлагает применить event sourcing ко всем сущностям в B2B биллинговой платформе: заказам, позициям, счетам, платёжным записям, пользовательским предпочтениям, настройкам уведомлений UI и сессионным токенам. Каков правильный ответ и как на самом деле должно приниматься это решение?
Честное руководство о том, когда применять event sourcing:
Event sourcing хорошо подходит когда:
- История переходов состояния сущности — требование комплаенса или законодательства (финансовые транзакции, медицинские записи, регулируемые решения)
- Временны́е запросы к прошлым состояниям нужны в продакшне, а не только для отладки
- Домен требует различать разные бизнес-причины для одного и того же результирующего состояния (OrderApproved рецензентом vs AutoApproved правилом)
- У команды есть реальный опыт с паттерном — event sourcing, принятый командой, никогда не поддерживавшей pipeline projection, несёт высокий риск
Event sourcing плохо подходит когда:
- Сущность активно изменяется в CRUD-стиле без значимой истории (предпочтения, настройки, данные сессии, записи кеша)
- Основное требование — быстрые, гибкие запросы текущего состояния; требований к аудиту нет
- Команда ещё не имеет опыта с инфраструктурой проекций и управлением лагом
- Систему нужно сдать в сжатые сроки — event sourcing существенно увеличивает начальное время разработки
Разработчик утверждает: «Event sourcing и CQRS — один паттерн, нельзя использовать один без другого». Какова правильная позиция и почему это различие важно?
- 01Что такое crypto-shredding и почему он является стандартным GDPR-ответом для event-sourced систем?
- 02Плохое событие было добавлено в журнал из-за ошибки инженера поддержки. Почему его нельзя просто удалить и каков правильный ответ?
- 03Назовите три категории сущностей, для которых event sourcing плохо подходит, и объясните почему.
Три пожара, одна причина: команда приняла event sourcing без понимания его структурных ограничений.
GDPR-пожар был предсказуем. Персональные данные в неизменяемом журнале событий нельзя удалить. Crypto-shredding — шифрование персональных данных per-subject, уничтожение ключа при удалении — это структурное решение, но его нужно проектировать с первого дня. Ретрофит болезненен или невозможен для событий, уже хранящихся в открытом тексте.
Пожар с плохим событием тоже был предсказуем. Журнал событий неизменяем. Инструментарий операторов, пишущий напрямую в него, должен обрабатываться с продакшн-дисциплиной. Когда плохое событие попадает в журнал, ответ — compensating event: новый факт, отменяющий бизнес-эффект. История сохраняется; ошибка и её исправление видны оба.
Пожар с лагом projection был провалом мониторинга. Eventual consistency между журналом событий и проекциями — структурная особенность. Под нагрузкой лаг растёт. Лаг projection нужно мониторить как метрику первого класса, масштабировать на пиковую пропускную способность и показывать в UI там, где это важно.
Event sourcing и CQRS — независимые паттерны, хорошо компонующиеся, но не одно и то же. Event sourcing оправдывает свои издержки для агрегатов с реальными требованиями аудита, временны́х запросов или фиксации намерений. Для предпочтений, данных сессий и простых CRUD-сущностей издержки реальны, а пользы нет.
Структурный трек заканчивается здесь. Следующий юнит (09) переходит от event sourcing — как хранится состояние — к событийно-ориентированной архитектуре: как модули общаются через события и как это формирует связанность.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.