Получить доступ

Проектирование событий и команд

Event vs command, обязательные поля envelope, idempotency key, schema evolution, thick vs thin payload и transactional outbox.

Event или command

Event сообщает о СВЕРШИВШЕМСЯ факте: OrderPaid, UserRegistered. Адресат — кто угодно, кто хочет реагировать; реакций может быть несколько. Command — намерение выполнить действие: ChargeCard, SendEmail. Адресат — один конкретный исполнитель.

Смешивание этих смыслов даёт неясные правила ретраев и подписки. Повтор события безопасен при идемпотентной обработке; повтор команды может создать второй побочный эффект (второе списание), если нет idempotency key. Имена тоже отражают это: события в прошедшем времени (OrderPaid), команды в повелительном (PayOrder).

Envelope: обязательные поля

Сообщение делят на envelope (метаданные) и payload (тело). В envelope кладут: eventId (UUID, для дедупа), eventType + schemaVersion (для эволюции), occurredAt (бизнес-время), aggregateId (для партиционирования и порядка per-key), correlationId/causationId (трассировка цепочек запрос→событие→событие), source/producer-id.

Эти поля важнее «красивого» формата payload: без них дальше нельзя ни дедуплицировать, ни эволюционировать схему, ни понять, что породило событие в логах. Структуру envelope обычно стандартизируют по всей компании — CloudEvents или собственный аналог.

Idempotency key

At-least-once доставка означает, что consumer обязан переживать дубли (см. урок про DLQ/retries). Технически — кладут стабильный idempotency key (обычно eventId) и держат таблицу processed_events с unique-constraint по нему.

Ключевое: запись бизнес-эффекта и отметки об обработке должны идти в ОДНОЙ транзакции БД (или эквиваленте). Если упасть между ними, при повторе либо обе записи появятся снова, либо обе отсутствуют — третьего не дано, иначе дубли в проде.

-- атомарно: эффект и отметка вместе
begin;
  insert into processed_events(event_id) values (?) on conflict do nothing returning 1;
  -- если вставка не дала строк — событие уже обрабатывали, пропускаем эффект
  insert into payments(...) values (...);
commit;

Schema evolution

События живут дольше одной версии сервиса: продюсер уже выкатил v2, а старые consumer'ы ещё читают v1. Поэтому формат событий проектируют под эволюцию: добавление optional-поля — обратно совместимо (старые читатели его игнорируют), а удаление поля или смена типа требует плана миграции и согласования с потребителями.

Schema Registry с правилами совместимости (BACKWARD/FORWARD/FULL) превращает договорённость в проверяемый контракт: попытка опубликовать несовместимую схему отклоняется на лету. Avro/Protobuf со Schema Registry — стандартный набор; чистый JSON работает, но контракт держится только на дисциплине.

Thick против thin payload

Сколько данных класть в событие — компромисс. Thick (event-carried state transfer): везти все нужные подписчикам поля внутри события, consumer'у не надо звать продюсера. Плюс — автономность; минус — раздутые события и сложнее эволюционировать.

Thin (event notification): только идентификаторы, consumer сам подтягивает детали через API/БД. Плюс — компактность и одна модель данных у продюсера; минус — связность сервисов и нагрузка от вторичных вызовов. Выбор по нагрузке и автономности подписчиков. И отдельно — PII/чувствительные данные в payload требуют осторожности: в логах брокера, ретеншне и архивах они живут долго.

Transactional outbox

Классическая проблема: записали в БД, но не отправили событие в брокер (или наоборот). Двухфазный коммит между БД и брокером дорог и хрупок — его не делают.

Решение — outbox: в той же транзакции БД, что и бизнес-данные, пишут строку в таблицу outbox. Отдельный publisher (или CDC поверх WAL — Debezium) читает outbox и публикует в брокер, помечая отправленные. Если publisher упадёт между транзакцией и отправкой, событие останется в outbox и уедет позже. Гарантия — «либо оба, либо ни одного», без 2PC.

Как отвечать на собеседовании

Разграничьте event (свершившийся факт, много подписчиков, идемпотентный повтор) и command (намерение, один исполнитель, требуется idempotency key). Перечислите обязательные поля envelope (eventId/Type/Version/OccurredAt/aggregateId/correlationId). Объясните идемпотентную обработку с атомарной записью эффекта+отметки. Подчеркните schema evolution через Registry с правилами совместимости. Сравните thick и thin payload и завершите transactional outbox как стандартный паттерн «БД + событие» вместо 2PC.

Вопросы по теме

Читайте также

Закрытая бета

Знать тему — половина дела

На живом собеседовании sotto слышит вопрос интервьюера и показывает короткий ответ и план. Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем