FinCodeCow · Глава II

Анатомия событийно-управляемой системы

От топологий до брокеров: как устроены фундаментальные строительные блоки современных EDM-архитектур

Если первая глава отвечала на вопрос «почему» бизнесу нужны событийно-управляемые микросервисы, то теперь пришло время разобрать «как» они устроены под капотом. Событийно-управляемая архитектура (EDA) — это не просто очередь сообщений, скрытая за красивым API. Это принципиально иная парадигма работы с данными, где каждый факт становится неизменяемой частью истории, а сервисы выступают в роли независимых наблюдателей и преобразователей этой истории.

• • •

IДва уровня топологии: внутренняя кухня и бизнес-карта

В мире EDM термин «топология» используется для описания архитектуры на двух совершенно разных уровнях. Понимание разницы между ними критически важно для проектирования.

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

Filter

Отбрасывание событий, не релевантных для данного сервиса.

Map

Преобразование структуры события — добавление вычисляемых полей, смена формата.

Join

Обогащение события данными из другого потока или локального стейта.

Aggregate

Свёртка нескольких событий в одно — например, подсчёт суммы заказа.

Split

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

Window

Группировка событий по временному окну для аналитики и паттернов.

Бизнес-топология — это «карта города». Она показывает, как множество независимых микросервисов, потоков событий и REST API взаимодействуют друг с другом для выполнения сложных сквозных бизнес-процессов. Здесь микросервисы — это узлы, а потоки событий — дороги между ними.

Принципиальное отличие бизнес-топологии EDA от обычной диаграммы REST-вызовов заключается в модели распространения данных. В REST-сети каждый вызов — это «точка-точка»: сервис А вызывает сервис Б, и больше никто об этом не знает. В событийной топологии один поток могут одновременно читать десятки независимых потребителей в режиме pub-sub: сервис уведомлений, сервис аналитики, сервис биллинга и поисковый индексатор — все подписаны на один и тот же поток OrderEvents, и ни один из них не знает о существовании остальных.

Ключевое отличие

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

• • •

IIАнатомия события: больше, чем просто сообщение

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

В практике EDA события принято классифицировать не по наличию ключа, а по семантике — тому, какой объём информации они несут и как потребители должны с ними работать.

Тип события Что несёт Плюсы Минусы
Domain Event Полный бизнес-факт: ID, контекст, все поля Потребитель обрабатывает без дополнительных запросов Большой объём, потенциальная утечка чувствительных данных
Event Notification Лёгкий сигнал: ID + что изменилось Минимум данных, защита приватности Дополнительная нагрузка на источник, риск thundering herd
Event-Carried State Transfer Snapshot или delta состояния сущности Максимальная развязка, потребителю не нужен источник Дублирование состояния, объём

Domain Events — значимые бизнес-факты, о которых заботятся эксперты предметной области. Например, OrderPlaced включает ID заказа, ID клиента, список позиций, сумму и временную метку. Это «богатые» события, которые несут в себе полноценный контекст произошедшего изменения.

Event Notification Events — лёгкие сигналы, которые лишь сообщают, что что-то произошло, не передавая подробностей. Например, CustomerProfileUpdated может содержать только ID клиента и секцию, которая изменилась (ADDRESS), заставляя потребителя запросить актуальное состояние через API.

Event-Carried State Transfer Events — события, которые несут внутри себя snapshot или delta состояния сущности, позволяя потребителям обновлять свои локальные проекции без обращения к источнику. Именно этот тип событий лежит в основе материализованных проекций и CQRS.

Независимо от семантического типа, событие обычно содержит ключ — идентификатор сущности, к которой оно относится. Ключ определяет, в какую партицию потока попадёт событие, и гарантирует, что все события, относящиеся к одному объекту или пользователю, будут обрабатываться в строгом порядке.

• • •

IIIМагия дуальности «Таблица-Поток»

Одна из самых мощных концепций в EDM — это возможность материализовать поток событий в таблицу состояний (просто применяя события по порядку) и, наоборот, превращать изменения в таблице обратно в поток событий. Поток — это история изменений; таблица — это текущее состояние, вычисленное из этой истории. Это две формы одних и тех же данных, и переход между ними — чистая функция.

Практическое применение этой дуальности — паттерн CQRS (Command Query Responsibility Segregation). Сервис подписывается на поток событий (например, OrderEvents), материализует из него локальную таблицу-проекцию (read model) и отдаёт её через REST API. Запись и чтение разделены: команды изменяют состояние через события, а запросы читают из оптимизированных проекций.

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

Read model — это проекция, а не реплика

Данные не просто копируются — они фильтруются, агрегируются и пересчитываются под нужды конкретного потребителя. Например, сервис рекомендаций может построить из потока заказов таблицу «топ-10 категорий по частоте покупок для каждого клиента», которая физически не существует в источнике.

Если сущность удаляется, в поток отправляется специальное событие — tombstone (метка об удалении). Потребители, материализовавшие проекции из этого потока, при получении tombstone очищают соответствующие записи в своих локальных хранилищах. В Kafka для этого существует режим log compaction: брокер сохраняет только последнее значение для каждого ключа, удаляя промежуточные записи, что превращает поток в компактный snapshot текущего состояния.

• • •

IVБрокер событий vs брокер сообщений

Многие разработчики по привычке путают брокеры событий (Event Brokers, например, Apache Kafka, Redpanda) с классическими брокерами сообщений (Message Brokers, например, RabbitMQ, ActiveMQ). Разница между ними фундаментальна.

Брокер сообщений

Работает как диспетчер: получает сообщение, ждёт, пока потребитель его заберёт, и уничтожает после подтверждения. Модель push или pull, но сообщение одноразовое.

Брокер событий

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

Ключевые требования к современному брокеру событий:

  1. Партиционирование — разделение потоков на подпотоки (партиции) для параллельной обработки. Количество партиций задаёт верхнюю границу параллелизма: если в потоке 6 партиций, то максимум 6 потребителей в группе смогут обрабатывать события одновременно.
  2. Строгая упорядоченность — внутри одной партиции события всегда идут в том же порядке, в котором были опубликованы. Глобального порядка между партициями не существует.
  3. Неизменяемость — опубликованное событие нельзя изменить. Можно только добавить новое.
  4. Бесконечное хранение и воспроизводимость — возможность прочитать данные в любой момент времени, с любого offset.
• • •

VСравнение брокеров: что выбрать

Выбор брокера — не религиозный вопрос, а инженерный. Ниже — сравнение основных игроков рынка.

Брокер Модель Когда выбирать Ограничения
Apache Kafka Распределённый лог, pull-модель Потоки событий, Event Sourcing, CQRS, аналитика, миллионы сообщений/сек Сложная эксплуатация, не для простых очередей задач
Redpanda Kafka-совместимый, C++ Низкая задержка, простота эксплуатации, замена Kafka без переписывания кода Меньше экосистема, не все Kafka-фичи 1:1
Apache Pulsar Лог + очереди, multi-tenancy Мультиарендность, геораспределение, tiered storage Меньше специалистов, сложнее в настройке
NATS JetStream Лёгкий стриминг Edge, IoT, микросервисы с низкой задержкой Меньше гарантий и инструментов для тяжёлого Event Sourcing
RabbitMQ Классический брокер, push-модель Очереди задач, RPC, маршрутизация по правилам Сообщение уничтожается после ack — не для Event Sourcing
AWS EventBridge Managed event bus Serverless, AWS-нативные системы Vendor lock-in, ограничения на порядок и хранение

Практическое правило: если нужна очередь задач — RabbitMQ или SQS. Если нужен поток фактов — Kafka, Redpanda, Pulsar или EventStoreDB. Смешивать роли в одном инструменте — путь к distributed big ball of mud.

• • •

VIПаттерны потребления: потоки против очередей

Брокер событий предоставляет данные потребителям двумя основными способами, в зависимости от бизнес-задачи.

Потребление в качестве потока событий (Event Stream)

Каждый потребитель сам управляет своим «смещением» (offset) — указателем на то, какое событие он прочитал последним. Несколько независимых групп потребителей (consumer groups) могут читать один и тот же поток, не мешая друг другу: каждая группа поддерживает собственный offset и движется в своём темпе.

Например, сервис инвентаризации и сервис уведомлений могут быть двумя разными consumer groups, читающими один и тот же топик OrderEvents. Сервис инвентаризации обрабатывает события синхронно и держит offset близко к «хвосту» потока. Сервис уведомлений может отставать на часы при пиковых нагрузках — и это нормально, события не потеряются.

Потребление в форме очереди (Queue)

Событие обрабатывается только одним экземпляром потребителя из пула. В Kafka это достигается через consumer group: все консьюмеры с одинаковым group.id делят партиции топика между собой. Если потребителей больше, чем партиций, «лишние» простаивают. Если потребитель падает, его партиции перераспределяются — этот процесс называется rebalance.

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

Критический нюанс rebalance

При eager rebalance все потребители в группе временно останавливают чтение, отдают партиции и получают новые. Это окно недоступности может длиться секунды. Cooperative Sticky Assignor минимизирует проблему, перераспределяя только нужные партиции, — остальным не нужно останавливаться. А static group membership (group.instance.id) позволяет потребителю при рестарте получить те же партиции без полного ребаланса.

Управление offset

Offset — это позиция потребителя в партиции. Kafka не отслеживает прогресс потребителя автоматически — это ответственность самого потребителя. Потребитель регулярно коммитит offset, отмечая, какие события он успешно обработал.

Стратегия Гарантия Риск
Auto-commit (до обработки) At-most-once Потеря сообщений при crashes между коммитом и обработкой
Manual commit (после обработки) At-least-once Дубликаты при crash до коммита
Transactional (Kafka EOS) Exactly-once Сложность, накладные расходы

При rebalance потребитель, потерявший партицию до коммита, передаёт её новому владельцу, который начнёт с последнего закоммиченного offset — и события, уже обработанные, но не закоммиченные, будут обработаны повторно. Это нормально для at-least-once, но означает, что потребители должны быть идемпотентными.

• • •

VIIИдемпотентность: почему одно и то же событие придёт дважды

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

Защита от дубликатов строится на нескольких уровнях:

  1. Idempotent Producer — Kafka присваивает каждому продюсеру уникальный PID (Producer ID) и sequence number для каждой записи. Брокер отслеживает последовательность и отбрасывает дубликаты на уровне партиции. Включается настройкой enable.idempotence=true (с Kafka 3.0 — по умолчанию).
  2. Idempotent Consumer — потребитель сам отслеживает, какие события уже обработал, по уникальному идентификатору (event ID, хеш payload, комбинация ключа и версии). Реализуется через таблицу дедупликации в БД, upsert-семантику или окно дедупликации.
  3. Exactly-Once Semantics (EOS) — Kafka предоставляет транзакционный механизм: продюсер и потребитель координируются через transactional.id, обработка и коммит offset происходят атомарно.

Пример идемпотентного потребителя с таблицей дедупликации:

BEGIN TRANSACTION;

IF EXISTS (SELECT 1 FROM processed_events WHERE event_id = :id) THEN
    COMMIT;
    RETURN;
END IF;

INSERT INTO processed_events (event_id, processed_at)
VALUES (:id, NOW());

UPDATE balances SET amount = amount + :delta
WHERE account_id = :account_id;

COMMIT;

Практическое правило: at-least-once + идемпотентный потребитель = effectively exactly-once. Это проще и надёжнее, чем транзакционный EOS, для большинства сценариев.

• • •

VIIIOut-of-order delivery: sequence numbers и коррекция

Даже при строгой упорядоченности внутри партиции, потребитель может столкнуться с нарушением порядка на нескольких уровнях:

  • При rebalance партиции перераспределяются, и новый владелец может начать с позиции, предшествующей тому, где остановился предыдущий.
  • При параллельной обработке внутри одного потребителя события из одной партиции могут быть обработаны не по порядку.
  • При кросс-партиционном чтении глобальный порядок не гарантируется: событие из партиции 1 может быть обработано раньше события из партиции 0.

Защита — встраивание sequence number в payload события. Потребитель сравнивает полученный номер с ожидаемым и:

Пробел обнаружен

Ожидаемый 5, получен 7. Потребитель либо ждёт событие 6 в буфере, либо фиксирует алерт и переходит в режим восстановления.

Устаревшее событие

Ожидаемый 7, получен 5. Событие уже обработано или потеряно. Потребитель отбрасывает его, чтобы не откатить состояние.

Sequence number может быть:

  • Монотонным счётчиком на сущность — генерируется продюсером, хранится в БД рядом с сущностью. Надёжно, но требует транзакционной записи.
  • Версией агрегата — в event sourcing каждая версия агрегата имеет номер, который увеличивается на 1 при каждом событии.
  • Timestamp + tiebreaker — слабее, но проще. Подходит для нестрогих сценариев, где допустимы мелкие нарушения порядка.

Для критичных сценариев (финансовые транзакции, инвентаризация) это необходимая мера. Для аналитики и уведомлений — как правило, избыточная.

• • •

IXSchema Registry: контракты событий как публичный API

Событие, пересекающее границу сервиса, — это публичный контракт. У него есть потребители, которых вы часто не контролируете и не всегда знаете. И как любой API, контракт события придётся менять: добавлять поля, переименовывать, дробить. Без формализованного управления схемами эволюция событий превращается в хаос — один производитель добавит поле, а десяток потребителей тихо упадёт при десериализации.

Schema Registry — централизованное хранилище схем событий (Confluent Schema Registry, Apicurio Registry, AWS Glue Schema Registry). Продюсер регистрирует схему, консьюмер по идентификатору в сообщении получает нужную версию.

Это даёт:

  • Проверку совместимости при публикации — registry отклонит несовместимое изменение схемы ещё на этапе деплоя, а не в продакшене у потребителя.
  • Компактность — в сообщении передаётся ID схемы, а не вся схема.
  • Единый источник правды о форматах событий.
Режим совместимости Что разрешено Когда использовать
Backward Новый потребитель читает старые события. Добавление необязательных полей, удаление полей. Потребители обновляются раньше продюсеров
Forward Старый потребитель читает новые события. Добавление полей, удаление необязательных. Продюсеры обновляются раньше потребителей
Full Оба направления. Самый безопасный, но и самый ограничительный. Независимый деплой продюсеров и консьюмеров
Backward Transitive Совместимость со всеми предыдущими версиями, а не только с последней. Event Sourcing, долгое хранение событий
Full Transitive Оба направления + все предыдущие версии. Максимальная безопасность, критичные системы

Для систем с event sourcing и долгим хранением критически важны transitive-режимы: они гарантируют, что новая версия схемы совместима со всеми предыдущими. Без этого потребитель, читающий события годовой давности, может не справиться с десериализацией.

• • •

XConsumer lag и DLQ: здоровье потока и обработка ошибок

Consumer lag — главная метрика здоровья EDA

Consumer lag — это разница между последним опубликованным событием в партиции (log-end-offset) и последним обработанным событием потребителя (current-offset). Если lag растёт — потребитель не успевает. Если lag стабилен и близок к нулю — система здорова.

lag = log-end-offset - current-offset

Инструменты мониторинга:

  • Burrow — специализированный мониторинг lag от LinkedIn, оценивает здоровье потребителей по sliding window.
  • Kafka Lag Exporter — экспорт метрик lag в Prometheus.
  • Confluent Control Center — коммерческий мониторинг с алертами.
  • Kafka Exporter — open-source экспортёр для Prometheus + Grafana.

Пороги зависят от сценария:

Сценарий Нормальный lag Алерт
Real-time (инвентаризация, платежи) 0–100 событий >1000 или рост за 5 минут
Near-real-time (уведомления) <1000 >10000 или рост за 15 минут
Batch (аналитика, индексация) <100000 >1000000 или рост за час

Dead Letter Queue — куда девать «ядовитые» события

Рано или поздно потребитель получит событие, которое не может обработать: битая схема, логическая ошибка, отсутствующая сущность. Если такое событие блокирует партицию, весь поток встаёт. Решение — Dead Letter Queue (DLQ).

Как это работает:

  1. Потребитель получает событие и пытается его обработать.
  2. Обработка падает. Событие уходит в retry topic с задержкой (например, 5 минут, 30 минут, 2 часа).
  3. После N попыток событие отправляется в DLQ — отдельный топик для ручного разбора.
  4. Потребитель продолжает обрабатывать следующие события — поток не блокируется.

Что важно в дизайне DLQ:

  • Метаданные — сохраняйте не только payload, но и причину ошибки, offset, timestamp, ID потребителя.
  • Алертинг — любое событие в DLQ должно порождать алерт. Если в DLQ тихо, а вы об этом не знаете — вы уже проиграли.
  • Replay-механизм — должна быть возможность вернуть событие из DLQ в исходный поток после исправления бага или данных.
  • Политика retention — DLQ должна хранить данные долго (месяцы), чтобы успеть разобраться.
Правило

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

• • •

XIИнфраструктурный фундамент и «налог на микросервисы»

Событийно-управляемые микросервисы не живут в вакууме. Им нужна инфраструктура. Сегодня стандартом де-факто для упаковки и оркестрации этих сервисов являются контейнеры (Docker) и системы управления контейнерами (Kubernetes). Они позволяют сервисам масштабироваться, восстанавливаться после сбоев и изолироваться друг от друга.

Но контейнеризация — лишь верхушка айсберга. Полноценная EDM-инфраструктура включает:

Брокер событий

Kafka / Redpanda / Pulsar — ядро системы, хранящее и доставляющее потоки.

Schema Registry

Управление контрактами событий и их эволюцией.

CDC-коннекторы

Debezium, Kafka Connect — захват изменений из реляционных БД и трансляция их в события.

Пайплайн обработки

Kafka Streams, Apache Flink — оконные агрегации, joins между потоками, CEP.

Observability

OpenTelemetry, мониторинг lag, DLQ, алерты на аномалии в throughput.

CI/CD с проверкой схем

Пайплайны, валидирующие совместимость схем через registry до деплоя.

«Налог на микросервисы» реален

Переход на EDM требует уплаты совокупности затрат — финансовых, временных, интеллектуальных — на поддержку всего вышеперечисленного. Малым командам этот налог может оказаться неподъёмным, и им лучше начать с модульного монолита, выделив границы bounded contexts внутри одного приложения и используя внутри-процессную шину событий.

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

• • •

XIIСинтез: связь с первой главой и фундамент для следующих

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

В первой статье мы говорили о законе Конвея и о том, что архитектура отражает структуру команд. Теперь мы можем увидеть это на конкретном уровне:

Организационный уровень (глава I) Технический уровень (глава II)
Bounded Context (DDD) Consumer Group — одна команда владеет одним потоком и его потребителями
Team Ownership Topic Ownership — команда отвечает за схему, retention, партиционирование
Value Stream Event Stream — поток фактов, отражающий бизнес-процесс
Team Topologies Бизнес-топология — сеть pub-sub-подписок между сервисами
Reverse Conway Maneuver Schema Registry + CI/CD — контракты как границы команд

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

Ключевые выводы главы:

  1. Топология EDA работает на двух уровнях: внутреннем (граф операций над событиями) и бизнесовом (сеть pub-sub-подписок, принципиально отличная от REST-сетки вызовов).
  2. События классифицируются по семантике: domain events (богатые факты), notification events (лёгкие сигналы), event-carried state transfer (события с полным состоянием). Выбор типа влияет на связанность и нагрузку.
  3. Дуальность «таблица-поток» — основа CQRS и материализованных проекций: сервисы делятся состоянием исключительно через потоки, без прямых обращений к чужим базам.
  4. Брокер событий принципиально отличается от брокера сообщений: неизменяемый журнал фактов, а не диспетчер с уничтожением после прочтения.
  5. Партиционирование задаёт предел параллелизма: количество партиций — это потолок для активных потребителей в группе.
  6. Идемпотентность потребителей — не опция, а необходимость: at-least-once — реальность, и потребитель должен быть готов к дубликатам.
  7. Schema Registry превращает хаотичную эволюцию событий в управляемый процесс с проверкой совместимости на этапе деплоя.
  8. Consumer lag и DLQ — обязательные элементы production-системы. Без мониторинга lag вы не знаете, что происходит. Без DLQ — теряете события, которые не смогли обработать.
  9. Инфраструктурный налог включает не только брокер, но и registry, CDC, observability и CI/CD со схемами. Для малых команд модульный монолит — разумная стартовая точка.
Что дальше

Понимание этих механизмов — фундамент, без которого невозможно проектировать надёжные, масштабируемые и отказоустойчивые бизнес-топологии. В следующей главе мы перейдём от анатомии к физиологии: как эти строительные блоки объединяются в конкретные паттерны — Saga, Outbox, Event Sourcing — и как выбрать правильный паттерн под конкретную бизнес-задачу.

Глава II цикла «Событийно-управляемая архитектура». Источники: Confluent, Redpanda, Conduktor, Architecture Weekly, Microsoft Azure Architecture Center, практические кейсы production-систем.

FinCodeCow · Chapter II

The Anatomy of an Event-Driven System

From topologies to brokers: the fundamental building blocks of modern EDM architectures

If Chapter I answered the "why" — why businesses need event-driven microservices — now it is time to dig into the "how": how they are built under the hood. Event-Driven Architecture (EDA) is not just a message queue hidden behind a nice API. It is a fundamentally different paradigm of working with data, where every fact becomes an immutable part of history, and services act as independent observers and transformers of that history.

• • •

ITwo Levels of Topology: The Inner Kitchen and the City Map

In the EDM world, the term "topology" is used to describe architecture at two completely different levels. Understanding the difference is critical for design.

Microservice topology is the "inner kitchen" of a single service. It describes how incoming events are filtered, transformed, enriched, and eventually published to outgoing streams. This is a graph of data-driven operations where nodes are standard transformations:

Filter

Discarding events not relevant to the service.

Map

Transforming the event structure — adding computed fields, changing format.

Join

Enriching an event with data from another stream or local state.

Aggregate

Folding multiple events into one — e.g., computing an order total.

Split

Breaking one event into several — e.g., by order line items.

Window

Grouping events by time window for analytics and pattern detection.

Business topology is the "city map." It shows how a multitude of independent microservices, event streams, and REST APIs interact to execute complex end-to-end business processes. Here, microservices are nodes and event streams are the roads between them.

The fundamental difference between EDA business topology and a regular REST call diagram lies in the data distribution model. In a REST network, every call is point-to-point: service A calls service B, and no one else knows about it. In an event topology, a single stream can be read simultaneously by dozens of independent consumers in pub-sub mode: notification service, analytics service, billing service, and search indexer — all subscribed to the same OrderEvents stream, none aware of the others.

The Key Difference

Adding a new consumer requires neither changing the producer's code nor coordinating between teams. Instead of a rigid call graph, you get a flexible subscription network. This is what makes event topology fundamentally different.

• • •

IIThe Anatomy of an Event: More Than Just a Message

An event is a statement of fact. Unlike classic messaging systems where a message is a command — "do something" — an event says: "here is what has already happened." Events are always named in the past tense (OrderPlaced, PaymentProcessed, CustomerRegistered), not in the imperative — this is not accidental but to emphasize their nature: the fact is already committed and cannot be undone.

In EDA practice, events are classified not by whether they carry a key but by semantics — how much information they carry and how consumers should work with them.

Event Type Payload Pros Cons
Domain Event Full business fact: ID, context, all fields Consumer processes without additional queries Large volume, potential sensitive data leak
Event Notification Lightweight signal: ID + what changed Minimal data, privacy protection Extra load on the source, thundering herd risk
Event-Carried State Transfer Snapshot or delta of entity state Maximum decoupling, consumer doesn't need the source State duplication, volume

Domain Events — significant business facts that domain experts care about. For example, OrderPlaced includes order ID, customer ID, line items, total, and timestamp. These are "rich" events carrying full context.

Event Notification Events — lightweight signals that only announce something happened, without details. For example, CustomerProfileUpdated may carry only the customer ID and the section that changed (ADDRESS), forcing the consumer to query the current state via API.

Event-Carried State Transfer Events — events that carry a snapshot or delta of entity state inside, allowing consumers to update their local projections without contacting the source. This type underpins materialized projections and CQRS.

Regardless of semantic type, an event usually carries a key — the identifier of the entity it relates to. The key determines which partition the event lands in and guarantees that all events for the same object or user are processed in strict order.

• • •

IIIThe Magic of the Table-Stream Duality

One of the most powerful concepts in EDM is the ability to materialize an event stream into a state table (simply by applying events in order) and, conversely, turn table changes back into an event stream. A stream is the history of changes; a table is the current state computed from that history. They are two forms of the same data, and the transition between them is a pure function.

The practical application of this duality is the CQRS pattern (Command Query Responsibility Segregation). A service subscribes to an event stream (e.g., OrderEvents), materializes a local projection table (read model), and serves it through a REST API. Write and read are separated: commands change state through events, and queries read from optimized projections.

This allows independent scaling of write and read sides and adaptation of the projection structure to specific queries — denormalize, index, cache.

A Read Model Is a Projection, Not a Replica

Data is not just copied — it is filtered, aggregated, and recomputed for the needs of a specific consumer. For example, a recommendation service may build from the order stream a table "top-10 categories by purchase frequency for each customer" that does not exist in the source.

When an entity is deleted, a special event is sent to the stream — a tombstone (deletion marker). Consumers that materialized projections from this stream clear the corresponding records in their local stores upon receiving the tombstone. In Kafka, this is supported by log compaction: the broker keeps only the latest value for each key, deleting intermediate records and turning the stream into a compact snapshot of the current state.

• • •

IVEvent Broker vs Message Broker

Many developers habitually confuse event brokers (Apache Kafka, Redpanda) with classic message brokers (RabbitMQ, ActiveMQ). The difference is fundamental.

Message Broker

Works like a dispatcher: receives a message, waits for a consumer to pick it up, destroys it after acknowledgment. Push or pull model, but the message is one-shot.

Event Broker

Works like an immutable log of facts. Keeps every event forever (or per retention policy), letting new services replay history from any point.

Key requirements for a modern event broker:

  1. Partitioning — splitting streams into sub-streams (partitions) for parallel processing. The number of partitions sets the upper bound on parallelism: if a stream has 6 partitions, at most 6 consumers in a group can process events simultaneously.
  2. Strict ordering — within a single partition, events always go in the same order they were published. There is no global ordering between partitions.
  3. Immutability — a published event cannot be changed. Only new ones can be added.
  4. Infinite retention and replayability — the ability to read data at any moment, from any offset.
• • •

VComparing Brokers: What to Choose

Choosing a broker is not a religious question but an engineering one. Below is a comparison of the main players.

Broker Model When to Choose Limitations
Apache Kafka Distributed log, pull model Event streams, Event Sourcing, CQRS, analytics, millions of messages/sec Complex operation, not for simple task queues
Redpanda Kafka-compatible, C++ Low latency, simpler operation, drop-in Kafka replacement Smaller ecosystem, not all Kafka features 1:1
Apache Pulsar Log + queues, multi-tenancy Multi-tenancy, geo-distribution, tiered storage Fewer specialists, harder to tune
NATS JetStream Lightweight streaming Edge, IoT, low-latency microservices Fewer guarantees and tools for heavy Event Sourcing
RabbitMQ Classic broker, push model Task queues, RPC, rule-based routing Message is destroyed after ack — not for Event Sourcing
AWS EventBridge Managed event bus Serverless, AWS-native systems Vendor lock-in, ordering and retention limits

Rule of thumb: if you need a task queue — RabbitMQ or SQS. If you need a stream of facts — Kafka, Redpanda, Pulsar, or EventStoreDB. Mixing roles in one tool leads to a distributed big ball of mud.

• • •

VIConsumption Patterns: Streams vs Queues

An event broker provides data to consumers in two main ways, depending on the business task.

Consuming as an Event Stream

Each consumer manages its own "offset" — a pointer to the last event it read. Multiple independent consumer groups can read the same stream without interfering: each group maintains its own offset and moves at its own pace.

For example, an inventory service and a notification service can be two different consumer groups reading the same OrderEvents topic. The inventory service processes events synchronously and keeps its offset near the "tail" of the stream. The notification service may lag by hours under peak load — and that is fine, events are not lost, they just wait in the stream.

Consuming as a Queue

An event is processed by only one consumer instance from a pool. In Kafka, this is achieved via a consumer group: all consumers with the same group.id share the topic's partitions. If there are more consumers than partitions, the "extra" ones idle. If a consumer dies, its partitions are redistributed — this process is called rebalance.

Ordering may be violated across partitions, but fair load balancing is guaranteed. Ideal for background tasks, mailings, and integrations.

The Critical Rebalance Nuance

During an eager rebalance, all consumers in the group temporarily stop reading, give up partitions, and receive new ones. This downtime window can last seconds. Cooperative Sticky Assignor minimizes the problem by redistributing only the partitions that actually need to move — the rest do not stop. And static group membership (group.instance.id) lets a consumer get the same partitions back after restart without a full rebalance.

Offset Management

An offset is a consumer's position within a partition. Kafka does not track consumer progress automatically — that is the consumer's responsibility. The consumer periodically commits offsets, marking which events it has successfully processed.

Strategy Guarantee Risk
Auto-commit (before processing) At-most-once Message loss on crash between commit and processing
Manual commit (after processing) At-least-once Duplicates on crash before commit
Transactional (Kafka EOS) Exactly-once Complexity, overhead

During rebalance, a consumer that loses a partition before commit hands it to a new owner, who starts from the last committed offset — events already processed but not committed will be processed again. This is normal for at-least-once but means consumers must be idempotent.

• • •

VIIIdempotency: Why the Same Event Will Arrive Twice

In distributed systems, duplicates are not a bug but the norm. A consumer crashed before committing an offset, a rebalance happened, the message is processed again. A network failure triggered a producer retry, and the broker wrote the event twice. The result: the consumer will see the same event at least twice over the system's lifetime.

Deduplication protection is built on several levels:

  1. Idempotent Producer — Kafka assigns each producer a unique PID (Producer ID) and a sequence number for each record. The broker tracks the sequence and discards duplicates at the partition level. Enabled with enable.idempotence=true (default since Kafka 3.0).
  2. Idempotent Consumer — the consumer itself tracks which events it has processed via a unique identifier (event ID, payload hash, key + version combination). Implemented via a deduplication table in the DB, upsert semantics, or a deduplication window.
  3. Exactly-Once Semantics (EOS) — Kafka provides a transactional mechanism: producer and consumer coordinate via transactional.id, processing and offset commit happen atomically.

Example of an idempotent consumer with a deduplication table:

BEGIN TRANSACTION;

IF EXISTS (SELECT 1 FROM processed_events WHERE event_id = :id) THEN
    COMMIT;
    RETURN;
END IF;

INSERT INTO processed_events (event_id, processed_at)
VALUES (:id, NOW());

UPDATE balances SET amount = amount + :delta
WHERE account_id = :account_id;

COMMIT;

Rule of thumb: at-least-once + idempotent consumer = effectively exactly-once. This is simpler and more reliable than transactional EOS for most scenarios.

• • •

VIIIOut-of-Order Delivery: Sequence Numbers and Correction

Even with strict ordering within a partition, a consumer can encounter ordering violations at several levels:

  • On rebalance — partitions are redistributed, and the new owner may start from a position preceding where the previous one stopped.
  • On parallel processing inside a single consumer, events from the same partition may be processed out of order.
  • On cross-partition reading — global order is not guaranteed: an event from partition 1 may be processed before an event from partition 0.

Protection lies in embedding a sequence number in the event payload. The consumer compares the received number with the expected one and:

Gap Detected

Expected 5, received 7. The consumer either waits for event 6 in a buffer or raises an alert and enters recovery mode.

Stale Event

Expected 7, received 5. The event has already been processed or lost. The consumer discards it to avoid rolling back state.

Sequence numbers can be:

  • A monotonic per-entity counter — generated by the producer, stored in the DB alongside the entity. Reliable but requires transactional writes.
  • An aggregate version — in event sourcing, each aggregate version has a number that increments by 1 per event.
  • Timestamp + tiebreaker — weaker but simpler. Suitable for non-strict scenarios where minor ordering violations are tolerable.

For critical scenarios (financial transactions, inventory), this is a necessary measure. For analytics and notifications, it is usually overkill.

• • •

IXSchema Registry: Event Contracts as a Public API

An event crossing a service boundary is a public contract. It has consumers you often do not control and do not always know. And like any API, the event contract will need to change: fields added, renamed, split. Without formalized schema management, event evolution turns into chaos — one producer adds a field, and a dozen consumers silently fail on deserialization.

Schema Registry is a centralized repository of event schemas (Confluent Schema Registry, Apicurio Registry, AWS Glue Schema Registry). The producer registers a schema, and the consumer gets the needed version by the ID in the message.

This provides:

  • Compatibility check at publish time — the registry rejects an incompatible schema change at deploy time, not in production at the consumer.
  • Compactness — the message carries only the schema ID, not the entire schema.
  • A single source of truth for event formats.
Compatibility Mode What Is Allowed When to Use
Backward New consumer reads old events. Adding optional fields, removing fields. Consumers upgrade before producers
Forward Old consumer reads new events. Adding fields, removing optional fields. Producers upgrade before consumers
Full Both directions. Safest but most restrictive. Independent deploy of producers and consumers
Backward Transitive Compatibility with all previous versions, not just the latest. Event Sourcing, long event retention
Full Transitive Both directions + all previous versions. Maximum safety, critical systems

For systems with event sourcing and long retention, transitive modes are critical: they guarantee the new schema version is compatible with all previous ones. Without this, a consumer reading a year-old event may fail to deserialize it.

• • •

XConsumer Lag and DLQ: Stream Health and Error Handling

Consumer Lag — The Main Health Metric of EDA

Consumer lag is the difference between the last published event in a partition (log-end-offset) and the last processed event of the consumer (current-offset). If lag grows — the consumer cannot keep up. If lag is stable and near zero — the system is healthy.

lag = log-end-offset - current-offset

Monitoring tools:

  • Burrow — LinkedIn's specialized lag monitoring, evaluates consumer health via sliding window.
  • Kafka Lag Exporter — exports lag metrics to Prometheus.
  • Confluent Control Center — commercial monitoring with alerts.
  • Kafka Exporter — open-source exporter for Prometheus + Grafana.

Thresholds depend on the scenario:

Scenario Normal Lag Alert
Real-time (inventory, payments) 0–100 events >1000 or growing in 5 minutes
Near-real-time (notifications) <1000 >10000 or growing in 15 minutes
Batch (analytics, indexing) <100000 >1000000 or growing in an hour

Dead Letter Queue — Where to Put "Poison" Events

Sooner or later, a consumer will receive an event it cannot process: broken schema, logical error, missing entity. If such an event blocks the partition, the whole stream halts. The solution is a Dead Letter Queue (DLQ).

How it works:

  1. The consumer receives an event and tries to process it.
  2. Processing fails. The event goes to a retry topic with a delay (e.g., 5 minutes, 30 minutes, 2 hours).
  3. After N attempts, the event is sent to the DLQ — a separate topic for manual review.
  4. The consumer continues processing subsequent events — the stream is not blocked.

What matters in DLQ design:

  • Metadata — store not just the payload but also the error reason, offset, timestamp, consumer ID.
  • Alerting — any event in the DLQ should trigger an alert. If the DLQ is quiet and you don't know about it — you have already lost.
  • Replay mechanism — there must be a way to return an event from the DLQ to the original stream after fixing the bug or data.
  • Retention policy — the DLQ must retain data for a long time (months) to allow investigation.
Rule

The DLQ is not a dump but a manual review queue. If your DLQ has thousands of events and no one reviews them — consider that you don't have a DLQ, only lost data.

• • •

XIThe Infrastructure Foundation and the "Microservices Tax"

Event-driven microservices do not live in a vacuum. They need infrastructure. Today, the de facto standard for packaging and orchestrating these services is containers (Docker) and container management systems (Kubernetes). They allow services to scale, recover from failures, and isolate from each other.

But containerization is only the tip of the iceberg. A full EDM infrastructure includes:

Event Broker

Kafka / Redpanda / Pulsar — the core of the system, storing and delivering streams.

Schema Registry

Managing event contracts and their evolution.

CDC Connectors

Debezium, Kafka Connect — capturing changes from relational DBs and turning them into events.

Processing Pipeline

Kafka Streams, Apache Flink — windowed aggregations, joins between streams, CEP.

Observability

OpenTelemetry, lag monitoring, DLQ, alerts on throughput anomalies.

CI/CD with Schema Validation

Pipelines validating schema compatibility via registry before deploy.

The "Microservices Tax" Is Real

The transition to EDM requires paying a combination of costs — financial, temporal, intellectual — for maintaining all of the above. For small teams, this tax may be unaffordable, and they are better off starting with a modular monolith, drawing bounded context boundaries inside a single application and using an in-process event bus.

But for large organizations where speed and flexibility matter more than simplicity, this tax is an investment in unprecedented scalability.

• • •

XIISynthesis: Link to Chapter I and Foundation for the Next

Chapter II lays the technical foundation for the rest of the journey. But it also deepens the connection with Chapter I — the organizational one.

In the first article, we discussed Conway's Law and how architecture mirrors team structure. Now we can see this at a concrete level:

Organizational Level (Chapter I) Technical Level (Chapter II)
Bounded Context (DDD) Consumer Group — one team owns one stream and its consumers
Team Ownership Topic Ownership — the team owns schema, retention, partitioning
Value Stream Event Stream — a stream of facts reflecting a business process
Team Topologies Business Topology — the pub-sub subscription network between services
Reverse Conway Maneuver Schema Registry + CI/CD — contracts as team boundaries

Microservices in EDM are not just isolated pieces of code but nodes in the event stream graph. The event broker is not just a "pipe" for data but a reliable store of truth that decouples services in time and space.

Key takeaways of the chapter:

  1. EDA topology works at two levels: internal (the graph of operations over events) and business (the pub-sub subscription network, fundamentally different from the REST call grid).
  2. Events are classified by semantics: domain events (rich facts), notification events (light signals), event-carried state transfer (events with full state). The choice affects coupling and load.
  3. The table-stream duality underpins CQRS and materialized projections: services share state exclusively via streams, without direct access to each other's databases.
  4. An event broker is fundamentally different from a message broker: an immutable log of facts, not a dispatcher that destroys after reading.
  5. Partitioning sets the parallelism limit: the number of partitions is the ceiling for active consumers in a group.
  6. Consumer idempotency is not an option but a necessity: at-least-once is reality, and the consumer must be ready for duplicates.
  7. Schema Registry turns chaotic event evolution into a managed process with compatibility checks at deploy time.
  8. Consumer lag and DLQ are mandatory elements of a production system. Without lag monitoring you don't know what is happening. Without a DLQ you lose events you could not process.
  9. The infrastructure tax includes not just the broker but also registry, CDC, observability, and CI/CD with schemas. For small teams, a modular monolith is a reasonable starting point.
What's Next

Understanding these mechanisms is the foundation without which it is impossible to design reliable, scalable, and fault-tolerant business topologies. In the next chapter, we will move from anatomy to physiology: how these building blocks combine into specific patterns — Saga, Outbox, Event Sourcing — and how to choose the right pattern for a specific business task.

Chapter II of the "Event-Driven Architecture" series. Sources: Confluent, Redpanda, Conduktor, Architecture Weekly, Microsoft Azure Architecture Center, production system case studies.