Event-Driven Architecture — это не про «послать сообщение». Это про то, чтобы перестать спрашивать.
Если вы когда-нибудь писали систему, в которой при оформлении заказа сервис заказов зовёт сервис платежей, тот зовёт сервис склада, склад зовёт уведомления, а уведомления возвращают ошибку и всё разваливается — поздравляю, вы прошли обряд посвящения в request/response. И теперь самое время поговорить про событийную архитектуру.
Event-Driven Architecture (EDA, событийная архитектура) — это подход, при котором компоненты системы общаются не через прямые вызовы «эй, сделай вот это», а через факты о том, что что-то произошло. Сервис заказов не дёргает платежи. Он просто говорит во всеуслышание: «Заказ номер 42 создан». А платежи, склад и уведомления сами решают, интересно ли им это событие и что с ним делать.
Звучит как мелочь, но это меняет в системе примерно всё.
Что такое событие и чем оно отличается от команды
Прежде чем уйти в архитектуру, давайте разберёмся с базовой терминологией. В EDA крутятся два очень похожих, но принципиально разных понятия — команда (command) и событие (event).
Команда (command) — это указание сделать что-то. Адресовано конкретному получателю. Может быть отклонено.
Событие (event) — это факт, что что-то уже произошло. Не имеет конкретного получателя. Не может быть «отменено» — оно уже случилось.
Команда называется в повелительном наклонении: CreateOrder, ChargePayment, ReserveStock. Событие — в прошедшем времени: OrderCreated, PaymentCharged, StockReserved. Это не вопрос вкуса, а вопрос модели мышления.
Когда вы пишете команду, вы знаете кому. Когда вы публикуете событие — вам всё равно. Может, его обработают пять подписчиков. Может, ноль. Может, через год появится новый сервис, который тоже захочет реагировать на это событие, — и старый код менять не придётся (что, по моему опыту, случается ровно один раз перед увольнением автора).
Аналогия: офис и доска объявлений
Представьте офис. В классическом request/response мире вы подходите к коллеге: «Коля, оплати счёт номер 42». Коля либо оплачивает, либо отвечает «не могу, я в отпуске». Если Коля заболел, всё стоит. Если счёт нужно заодно отдать в бухгалтерию, в логистику и в архив — вы бегаете по офису.
В EDA вы вешаете объявление на доску: «Счёт номер 42 одобрен». Дальше каждый отдел смотрит на доску в свободное время и реагирует, если ему интересно. Бухгалтерия — оплачивает, логистика — резервирует машину, архив — подшивает. Никто ни у кого ничего не спрашивает.
И да — если завтра появится новый отдел «контроль качества», он тоже подпишется на ту же доску, и менять процессы остальных отделов не придётся.
Из чего состоит EDA-система
Любая событийная архитектура держится на трёх типах участников: продюсеры, брокер событий и консьюмеры. Плюс — иногда — хранилище событий (event store), но об этом ниже.
На диаграмме хорошо видно ключевую идею: продюсеры не знают консьюмеров. Order Service публикует OrderCreated и идёт обрабатывать следующий запрос. Дальше брокер сам разносит событие всем заинтересованным подписчикам — а их может быть один, четыре или сорок. Source code продюсера от этого не меняется.
Кто такой брокер и зачем он нужен
Брокер событий — это сердце EDA. Это middleware, который принимает события от продюсеров, сохраняет их и передаёт консьюмерам. Без брокера у вас не EDA, а просто набор сервисов, которые орут друг на друга через HTTP.
Самые популярные брокеры:
- Apache Kafka — стандарт индустрии для high-throughput сценариев, события хранятся долго (можно «переиграть» историю)
- RabbitMQ — классическая очередь сообщений, гибкая маршрутизация, но события не хранятся после обработки
- NATS / NATS JetStream — лёгкий и быстрый, набирает популярность
- AWS SNS/SQS, Google Pub/Sub, Azure Service Bus — managed-решения для облаков
Выбор брокера — это отдельная большая тема, которая зависит от нагрузки, требований к надёжности и того, нужно ли вам хранить историю событий. Здесь работает обычное правило: не берите Kafka, если у вас 10 событий в час (но обязательно скажите на собеседовании, что «у нас Kafka» — это солидно звучит).
Три основных паттерна EDA
Под зонтиком «event-driven» прячется несколько разных паттернов. Понимать разницу между ними важно, потому что от выбора зависит сложность системы.
Паттерн 1: Event Notification
Самый простой вариант. Продюсер публикует событие с минимумом данных (часто — только ID), а консьюмер при необходимости запрашивает детали обратно у продюсера.
OrderService → событие: { type: "OrderCreated", orderId: 42 }PaymentService → получает событие → дёргает GET /orders/42 → обрабатываетПлюсы: события маленькие, нет дублирования данных. Минусы: остаётся синхронная зависимость на этапе «дёрнуть детали», а значит, продюсер должен быть жив.
Паттерн 2: Event-Carried State Transfer
Событие содержит всё нужное состояние. Консьюмер не дёргает продюсера, а работает только с тем, что пришло в событии.
{ "type": "OrderCreated", "orderId": 42, "customerId": 17, "items": [{"sku": "ABC-1", "qty": 2, "price": 1500}], "totalAmount": 3000, "currency": "RUB", "shippingAddress": "..."}Плюсы: полная независимость консьюмеров, продюсер может быть в офлайне. Минусы: события «жирные», данные дублируются между сервисами, нужно следить за версионированием схемы события.
Паттерн 3: Event Sourcing
Самый радикальный подход: состояние системы хранится не как «текущая запись в БД», а как последовательность событий. Чтобы получить текущее состояние объекта, нужно проиграть все его события с самого начала.
Если у вас есть заказ, то в БД лежит не строка orders со статусом paid, а цепочка: OrderCreated → ItemAdded → ItemAdded → DiscountApplied → PaymentCharged. Текущий статус — это результат свёртки этой цепочки.
Это мощный, но очень сложный паттерн. Часто идёт в связке с CQRS (об этом будет отдельный пост). Для большинства задач Event Sourcing — это из пушки по воробьям, и применять его стоит, только когда вам реально нужна полная история изменений (банкинг, биллинг, аудит).
Как это всё работает: оформление заказа в e-commerce
Давайте разберём конкретный кейс. Пользователь оформляет заказ в интернет-магазине. В классической архитектуре цепочка вызовов могла бы выглядеть как «сервис заказов синхронно вызывает платежи, потом склад, потом уведомления». В EDA всё происходит иначе.
Что важно увидеть на этой диаграмме:
- Order Service отвечает пользователю мгновенно —
202 Accepted, как только сохранил заказ. Никаких ожиданий от платежей или склада. - Платежи и склад работают параллельно — а не один за другим. Это даёт лучшее время отклика всей системы.
- Order Service сам подписан на свои же события — он реагирует на
PaymentCompletedиStockReserved, чтобы обновить статус заказа. - Loyalty Service появился как новый подписчик — никто из старых сервисов о нём не знает и не должен знать. Подписали — работает.
Это и есть та самая «развязанность» (decoupling), за которую все так любят EDA на конференциях.
EDA vs Request/Response: когда что использовать
EDA — это не серебряная пуля. Есть задачи, где синхронный вызов лучше события. Давайте сравним подходы честно.
| Критерий | Request/Response (REST, gRPC) | Event-Driven |
|---|---|---|
| Связанность | Высокая: клиент знает сервер | Низкая: продюсер не знает консьюмеров |
| Латентность одной операции | Низкая (ждём ответ сразу) | Выше (асинхронно) |
| Отказоустойчивость | Падает один — падает цепочка | Брокер буферизирует, сервисы могут падать |
| Сложность отладки | Простая, один трейс | Сложная, нужны distributed tracing и корреляция |
| Согласованность данных | Транзакционная (если одна БД) | Eventual consistency |
| Расширяемость | Менять API, договариваться с клиентами | Просто добавить нового подписчика |
| Подходит для | Запросы с немедленным ответом, чтение данных | Бизнес-процессы, оркестрация, аналитика, интеграции |
Грубое правило: если пользователю нужно увидеть результат прямо сейчас (поиск, авторизация, чтение профиля) — это request/response. Если действие запускает цепочку «событий в реальной жизни» (заказ, платёж, доставка) — это EDA.
В реальных системах эти подходы прекрасно живут вместе. Внешний API может быть классическим REST’ом (про правила именования endpoint’ов писал отдельно), а вот внутреннее общение между сервисами — уже через события.
Связь EDA с DDD и микросервисами
Событийная архитектура — это не про микросервисы. Точнее, не только. EDA отлично работает и внутри модульного монолита, и в гибридных системах. Но особенно красиво она ложится на Domain-Driven Design, потому что событие — это естественный способ выразить доменный факт.
В DDD есть отдельное понятие — Domain Event. Это событие, важное с точки зрения бизнеса. Не «строчка добавилась в БД», а «клиент сделал заказ». Не «обновился флаг в таблице», а «платёж прошёл». Domain Event говорит языком предметной области, а не языком кода.
Когда вы проектируете bounded contexts, события становятся главным способом интеграции между ними. Контекст «Заказы» не лезет внутрь контекста «Склад» — он публикует событие, и контекст склада сам решает, что с ним делать. Так сохраняется автономность контекстов и не возникает «общего ядра», которое к третьему году разработки превращается в большой клубок зависимостей.
Event Storming как метод проектирования
Раз уж мы про DDD: есть методика под названием Event Storming — её часто используют, чтобы спроектировать систему «сверху вниз» через события. Команда собирается у стены (или в Miro, тут уж как офис позволяет), и все вместе наклеивают стикеры: какие события происходят в системе, в каком порядке, кто их генерирует, кто реагирует.
Это даёт две большие пользы. Первая — вы быстро находите границы bounded contexts (там, где меняется «язык» событий). Вторая — у бизнеса и разработки появляется один общий словарь. Ну и стикеры на стене эффектно выглядят на фотографиях для презентации (что тоже немаловажно для бюджета на следующий квартал).
Подводные камни EDA
Я не могу закончить пост, не показав обратную сторону медали. EDA — это не магия, у неё есть вполне ощутимые сложности.
1. Eventual consistency
Когда вы публикуете событие, вы не знаете, когда оно обработается. Может, через миллисекунду. Может, через пять секунд (если консьюмер тормозит). Поэтому забудьте про «прочитал сразу после записи и получил актуальное состояние». Системе нужно время, чтобы прийти в согласованное состояние.
Для пользователя это означает классическое: «я нажал кнопку, а на следующей странице ничего не изменилось». Решается это либо UX-приёмами («ваш заказ обрабатывается»), либо паттерном Read-Your-Writes на фронте.
2. Дублирование событий и идемпотентность
Брокеры в большинстве своём работают по принципу at-least-once delivery — событие гарантированно дойдёт, но может прийти несколько раз. Значит, каждый консьюмер должен быть идемпотентным: повторная обработка того же события не должна ломать систему.
Самое простое решение — хранить ID обработанных событий и игнорировать дубликаты. Но это надо помнить всегда. Иначе ваш платёжный сервис спишет деньги дважды, и беседа с финдиректором будет очень неприятной.
3. Версионирование схем событий
Через год у вас в OrderCreated появится новое поле. Через два — изменится структура. Старые консьюмеры должны продолжать работать, новые — использовать новые поля. Версионирование событий — это отдельная боль, для которой существуют такие штуки, как Schema Registry (Confluent Schema Registry для Kafka, например).
4. Сложная отладка
Когда в процессе оформления заказа задействованы 7 сервисов и 12 событий, найти причину бага без хороших инструментов — это пытка. Нужны:
- Distributed tracing (Jaeger, Zipkin, OpenTelemetry)
- Correlation ID в каждом событии — чтобы связать всю цепочку
- Централизованное логирование (ELK, Loki, Datadog)
- Мониторинг очередей — кто отстаёт, у кого растёт лаг
Без этого набора EDA превращается в чёрный ящик, и единственная стратегия отладки — «работает же, не трогай».
5. Saga-паттерн и распределённые транзакции
В EDA нет привычного BEGIN TRANSACTION ... COMMIT. Если в середине бизнес-процесса что-то сломалось, вам нужно делать компенсирующие действия — то есть откатывать состояние через ещё одни события. Это называется Saga и заслуживает отдельного поста.
Например: заказ создан → платёж прошёл → склад не смог зарезервировать товар → нужно публиковать RefundPaymentRequested и OrderCancelled. И не забыть отправить пользователю уведомление, что заказ отменён.
Заключение
Event-Driven Architecture — это не про «модно». Это про другой способ мышления. Вы перестаёте проектировать систему как набор взаимных вызовов и начинаете видеть её как поток фактов о бизнесе. Заказ создан. Платёж прошёл. Товар отгружен. А кто на эти факты как реагирует — это уже техническая деталь, которую можно менять, не ломая всё остальное.
Цена за это — eventual consistency, идемпотентность, версионирование, инструменты наблюдаемости и в целом более высокий порог входа. Но если ваша система действительно про бизнес-процессы, а не про CRUD — EDA окупится. Особенно когда через год к вам придут с фразой «нам нужно ещё подключить аналитику и партнёрский сервис», и вы вместо переписывания половины кода просто добавите двух новых подписчиков.
PS. Если вы сейчас читаете это и думаете «у нас всё на синхронных REST-вызовах, и всё работает» — поздравляю, вы ещё не доросли до проблем, которые решает EDA. Дорастёте — вспомните этот пост.