Содержание
Событийная архитектура — это подход, при котором части системы общаются, публикуя события о том, что произошло: «заказ оплачен», «пользователь зарегистрировался». Остальные части подписываются на нужные события и реагируют сами. Это снижает связанность: модуль оплаты не обязан знать о складе, уведомлениях и аналитике. Цена — асинхронность, которую нужно уметь делать надёжной.
Разберём, чем события отличаются от команд, как не потерять событие, как обрабатывать повторы и когда подход избыточен.
Что такое событийная архитектура?
Событийная архитектура — это способ строить систему вокруг фактов, которые в ней происходят. Вместо цепочки прямых вызовов «оплата вызывает склад, склад вызывает доставку» модуль оплаты публикует событие, а склад, доставка и уведомления обрабатывают его независимо друг от друга.
Что это даёт:
- слабая связанность — новые реакции на событие добавляются без изменения источника;
- устойчивость — если сервис уведомлений упал, оплата продолжает работать, а события дождутся его;
- масштабирование — тяжёлые обработчики выполняются в фоне и масштабируются отдельно;
- история — поток событий показывает, что и когда происходило в системе.
Чем событие отличается от команды?
Событие сообщает о свершившемся факте и не ждёт ответа, команда — просит кого-то сделать конкретное действие. Путаница между ними — частая причина запутанной архитектуры: если «событие» на самом деле требует определённого результата от конкретного получателя, это команда.
| Признак | Событие | Команда |
|---|---|---|
| Время | Прошедшее: 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,
// публикует их в брокер и помечает как отправленные
Так событие гарантированно появится, если изменение данных сохранилось, и не появится, если транзакция откатилась.
Как обрабатывать повторы и порядок событий?
Брокеры обычно гарантируют доставку «хотя бы один раз», поэтому одно событие может прийти дважды. Обработчики делают идемпотентными: перед обработкой проверяют, не обрабатывали ли уже событие с этим идентификатором. Подробнее о приёме — в статье идемпотентность платежей и вебхуков.
Правила надёжной обработки:
- У каждого события есть уникальный идентификатор и время создания.
- Обработчик записывает идентификаторы обработанных событий.
- Если важен порядок, события одной сущности направляют в один раздел или очередь.
- Ошибочные события после нескольких повторов уходят в отдельную очередь для разбора.
- Формат события версионируется, а старые версии поддерживаются, пока есть потребители.
Как выбрать брокер, мы разбирали в статье Kafka или RabbitMQ.
Когда событийная архитектура избыточна?
Событийная архитектура избыточна, когда системой занимается одна небольшая команда, бизнес-процессы простые и линейные, а пользователю нужен немедленный и согласованный результат. В таких случаях прямые вызовы проще писать, читать и отлаживать.
Признаки, что события пока не нужны:
- одна реакция на действие и она не изменится;
- результат обработки нужен пользователю сразу в том же запросе;
- нет нагрузки, ради которой стоит выносить работу в фон;
- нет мониторинга и трассировки — без них асинхронную систему трудно сопровождать.
Хороший промежуточный вариант — события внутри монолита через встроенный механизм фреймворка и очереди задач. Если позже часть системы выделится в отдельный сервис — подробнее в статье монолит или микросервисы, — события уже будут готовы.
Коротко
- Событийная архитектура связывает части системы через факты, а не прямые вызовы.
- Событие сообщает о произошедшем, команда просит выполнить действие.
- Паттерн Outbox гарантирует, что событие не потеряется между базой и брокером.
- Обработчики должны быть идемпотентными, а сбойные события — попадать в отдельную очередь.
- Для простых процессов и маленькой команды достаточно событий внутри монолита.
Частые вопросы
Нужен ли брокер сообщений для событийной архитектуры?
Внутри одного приложения события можно обрабатывать через встроенный механизм фреймворка и очередь задач. Брокер нужен, когда события передаются между отдельными сервисами или командами.
Как отлаживать систему на событиях?
Нужны сквозной идентификатор, который передаётся вместе с событием, журнал опубликованных и обработанных событий и трассировка. Без этого цепочку причин и следствий восстановить очень сложно.
Что делать с событием, которое не удаётся обработать?
После нескольких повторов его отправляют в отдельную очередь для разбора, а команда получает оповещение. После исправления ошибки событие можно обработать заново.



