Содержание

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

Разберём, чем события отличаются от команд, как не потерять событие, как обрабатывать повторы и когда подход избыточен.

Что такое событийная архитектура?

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

Что это даёт:

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

Чем событие отличается от команды?

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

Признак Событие Команда
Время Прошедшее: OrderPaid Повелительное: ReserveStock
Получатели Любое число, источник о них не знает Один конкретный исполнитель
Ответ Не ожидается Ожидается результат или ошибка
Может ли получатель отказаться Нет, факт уже произошёл Да

Про то, как выделять модули и границы между ними, — статья DDD простыми словами.

Пронумерованные почтовые конверты на деревянных полках Фото: Unsplash

Как не потерять событие?

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

DB::transaction(function () use ($order, $payment) {
    $order->markPaid($payment);
    $order->save();

    OutboxMessage::create([
        'type'    => 'order.paid',
        'payload' => ['order_id' => $order->id, 'amount' => $payment->amount],
    ]);
});

// Отдельный процесс читает неотправленные записи outbox,
// публикует их в брокер и помечает как отправленные

Так событие гарантированно появится, если изменение данных сохранилось, и не появится, если транзакция откатилась.

Как обрабатывать повторы и порядок событий?

Брокеры обычно гарантируют доставку «хотя бы один раз», поэтому одно событие может прийти дважды. Обработчики делают идемпотентными: перед обработкой проверяют, не обрабатывали ли уже событие с этим идентификатором. Подробнее о приёме — в статье идемпотентность платежей и вебхуков.

Правила надёжной обработки:

  1. У каждого события есть уникальный идентификатор и время создания.
  2. Обработчик записывает идентификаторы обработанных событий.
  3. Если важен порядок, события одной сущности направляют в один раздел или очередь.
  4. Ошибочные события после нескольких повторов уходят в отдельную очередь для разбора.
  5. Формат события версионируется, а старые версии поддерживаются, пока есть потребители.

Как выбрать брокер, мы разбирали в статье Kafka или RabbitMQ.

Когда событийная архитектура избыточна?

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

Признаки, что события пока не нужны:

  • одна реакция на действие и она не изменится;
  • результат обработки нужен пользователю сразу в том же запросе;
  • нет нагрузки, ради которой стоит выносить работу в фон;
  • нет мониторинга и трассировки — без них асинхронную систему трудно сопровождать.

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

Коротко

  • Событийная архитектура связывает части системы через факты, а не прямые вызовы.
  • Событие сообщает о произошедшем, команда просит выполнить действие.
  • Паттерн Outbox гарантирует, что событие не потеряется между базой и брокером.
  • Обработчики должны быть идемпотентными, а сбойные события — попадать в отдельную очередь.
  • Для простых процессов и маленькой команды достаточно событий внутри монолита.

Частые вопросы

  • Нужен ли брокер сообщений для событийной архитектуры?

    Внутри одного приложения события можно обрабатывать через встроенный механизм фреймворка и очередь задач. Брокер нужен, когда события передаются между отдельными сервисами или командами.

  • Как отлаживать систему на событиях?

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

  • Что делать с событием, которое не удаётся обработать?

    После нескольких повторов его отправляют в отдельную очередь для разбора, а команда получает оповещение. После исправления ошибки событие можно обработать заново.