коротко

Событие — это факт о прошлом: «заказ создан», «оплата прошла». Оно ничего ни от кого не требует, просто сообщает, что нечто случилось. Это противоположность команде — «создай заказ», «спиши деньги», где один сервис приказывает другому. В событийной архитектуре сервисы общаются именно событиями через брокер (например, Kafka): тот, кто событие породил, не знает, кто его прочитает и прочитает ли вообще. Это даёт слабую связанность — нового потребителя можно подключить, не трогая отправителя. Платят за это eventual consistency: согласованность наступает не мгновенно, единого «сейчас» в системе нет, а отладка усложняется. Для аналитика событие становится контрактом — у него есть имя, payload и смысл, которые нужно спроектировать.

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

Событие против команды

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

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

Проверка на грамматику

Если формулировка звучит как приказ («сделай X») — это команда, и у неё есть тот, кто обязан выполнить. Если как новость в прошедшем времени («X случилось») — это событие, и оно живёт само по себе. Хорошее имя события всегда в прошедшем времени: OrderCreated, PaymentSucceeded, а не CreateOrder. Это не педантизм — это способ не перепутать «прикажи» и «сообщи» в спеке.

Как это работает: продюсер, брокер, консьюмер

Механика стоит на трёх ролях, и они те же, что в асинхронной интеграции вообще — поэтому глубоко в очереди я тут не полезу, это разобрано в записи про синхронную и асинхронную интеграцию. Коротко:

  • Продюсер (producer) — сервис, который порождает событие. Сервис заказов оформил заказ и публикует «заказ создан».
  • Топик (topic) — именованный поток событий в брокере, куда продюсер пишет, а консьюмеры читают.
  • Консьюмер (consumer) — сервис, который подписан на топик и реагирует на события по-своему: бонусы начисляют баллы, уведомления шлют письмо, склад списывает остаток.

Ключевая идея: продюсер и консьюмеры не знают друг о друге. Сервис заказов не вызывает сервис бонусов — он вообще не подозревает, что тот существует. Он просто публикует факт, а брокер развозит его всем заинтересованным. Это и есть слабая связанность на уровне архитектуры.

flowchart LR
  P["Сервис заказов (продюсер)"] -->|"OrderCreated"| T["Топик: orders"]
  T --> C1["Бонусы"]
  T --> C2["Уведомления"]
  T --> C3["Склад"]
  T --> C4["Аналитика"]

На схеме сервис заказов публикует одно-единственное событие OrderCreated в топик orders. Из этого топика событие независимо забирают четыре консьюмера — бонусы, уведомления, склад и аналитика, — и каждый делает свою часть, не мешая остальным и не зная об их существовании. Сила картинки в том, что стрелка от продюсера всего одна, а получателей сколько угодно. Захотим завтра подключить пятого потребителя (скажем, антифрод) — подпишем его на тот же топик, и сервис заказов об этом даже не узнает: его код не меняется. В мире команд пришлось бы лезть в отправителя и дописывать ещё один вызов.

Зачем это нужно: слабая связанность

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

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

Чем за это платят: eventual consistency

Бесплатного не бывает. Главная цена событийной архитектуры — eventual consistency, «согласованность в конечном счёте».

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

«Сейчас» больше нет — и это вопрос аналитика

В событийной системе нет момента, в который всё одновременно верно. Пользователь может увидеть свой заказ раньше, чем за него начислятся бонусы. Это не баг — это природа подхода, и её надо явно проговорить в требованиях. Допустима ли задержка? Сколько секунд терпимо? Что показывать пользователю, пока баллы «в пути»? Если в спеке написано «после оформления заказа бонусы сразу видны в профиле» — а система событийная, — спека врёт о реальности, и кто-то на приёмке заведёт «баг», которого нет.

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

Тонкое событие или толстое: два стиля

События бывают двух фасонов, и выбор между ними — тоже работа аналитика.

Событийная нотификация (event notification) — тонкое событие: «заказ 123 создан», и всё. Кому нужны детали — идёт за ними в сервис заказов сам. Плюс: событие лёгкое, сервисы не таскают лишнего. Минус: каждый консьюмер делает дополнительный запрос за подробностями, и сервис-источник снова всем нужен — связанность подкрадывается обратно.

Event-carried state transfer — толстое событие: «заказ 123 создан», и внутри сразу состав, сумма, клиент, адрес. Консьюмеру не нужно никуда ходить — всё необходимое уже в событии. Плюс: полная независимость, источник можно хоть выключить. Минус: события тяжелее, а данные в них могут устареть к моменту обработки.

СтильЧто внутриПлюсМинус
Нотификациятолько факт + idлёгкое событиеконсьюмер ходит за деталями
State transferфакт + все данныеполная независимостьтяжёлое, данные могут устареть

Порядок и повторы: коротко

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

Что меняется для аналитика

Главный сдвиг: событие — это контракт, такой же, как эндпоинт API. У него есть три части, которые проектируете вы, а не «бэк сам разберётся»:

  • Имя — в прошедшем времени, отражающее факт: OrderCreated, а не «обнови заказ». Имя — это публичное обещание, менять его потом больно.
  • Payload — какие поля несёт событие. Это решение про тонкое/толстое: хватит ли потребителям id или нужны детали.
  • Смысл — в какой момент событие считается случившимся. «Заказ создан» — это когда нажали кнопку или когда прошла оплата? От ответа зависит вся логика консьюмеров.

И второе: eventual consistency должна жить в требованиях. Не «бонусы начисляются», а «бонусы начисляются в течение нескольких секунд после оформления; пока их нет, в профиле показываем „рассчитываем“». Вы описываете не мгновенный мир, а мир с задержками, — и честно говорите, какие задержки допустимы.

Откуда это взялось

Идея общения через сообщения, а не прямые вызовы, родилась ещё в 1990-х — это эпоха message-oriented middleware (MOM) и корпоративных шин данных: IBM MQ, JMS, позже ESB. Тогда это был тяжёлый интеграционный слой для больших предприятий. Новый виток случился около 2011 года, когда LinkedIn открыл Kafka: ей нужно было прокачивать гигантские потоки событий между сервисами дёшево и надёжно, и она сделала событийный подход массовым. Параллельно с ростом микросервисов события стали естественным клеем между ними. Рядом выросли родственные идеи: event sourcing (хранить не текущее состояние, а всю историю событий, приведших к нему) и CQRS (разделять модель записи и модель чтения) — оба опираются на тот же фундамент «факт о прошлом как единица обмена».

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

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

Команда — это приказ в повелительном наклонении («создай заказ»), у неё есть конкретный адресат, который обязан её выполнить, и отправитель от этого адресата зависит. Событие — это факт в прошедшем времени («заказ создан»), оно никому конкретно не адресовано и ничего не требует, просто сообщает, что нечто произошло. Отправитель события не знает, кто его услышит. На грамматике: приказ «сделай X» — команда, новость «X случилось» — событие.

Что такое eventual consistency простыми словами?

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

Зачем нужна событийная архитектура, если есть обычные вызовы?

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