Все статьи

Event-Driven Architecture: Kafka, RabbitMQ, Outbox Pattern и Eventual Consistency

Полное руководство для Full Stack разработчика: событийная архитектура, брокеры сообщений, FastAPI, PostgreSQL, Idempotent Consumers, Saga Pattern и надёжная обработка событий

27 минут чтения

Представим, что вы разрабатываете SaaS-приложение на React, FastAPI и PostgreSQL.

Пользователь регистрируется, создаёт организацию и оплачивает подписку.

После успешной оплаты приложение должно выполнить несколько действий:

  • активировать платный тариф;

  • отправить Email;

  • создать запись в Audit Log;

  • обновить аналитические показатели;

  • уведомить администратора;

  • синхронизировать данные с CRM.

На раннем этапе всё это можно реализовать внутри одного Backend Endpoint.

Но по мере роста приложения такой подход становится проблемой.

Например:

POST /api/billing/activate

|

v

Update Database

|

v

Send Email

|

v

Update CRM

|

v

Update Analytics

|

v

Return Response

Что произойдёт, если CRM недоступна?

Нужно ли отменять активацию подписки?

Что делать, если Email Service отвечает слишком медленно?

Как избежать повторной отправки уведомления?

Как гарантировать, что событие об успешной оплате не потеряется после записи в PostgreSQL?

И что произойдёт, если приложение получит одно событие дважды?

Для решения таких задач используется Event-Driven Architecture — событийная архитектура.

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

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

В этой статье разберём, как работает событийная архитектура веб-приложения, чем отличаются Kafka и RabbitMQ, зачем нужен Transactional Outbox Pattern, как реализовать Idempotent Consumer и когда использовать Eventual Consistency и Saga Pattern.


Часть 1. Основы событийной архитектуры

1. Что такое Event-Driven Architecture

Event-Driven Architecture (EDA) — архитектурный подход, при котором компоненты взаимодействуют через события, отражающие изменения состояния или произошедшие бизнес-факты.

Например:

UserRegistered

OrganizationCreated

SubscriptionActivated

PaymentCompleted

ReportGenerated

FileUploaded

Событие сообщает, что нечто уже произошло.

Например:

{

"event_type": "subscription.activated",

"event_id": "evt_123",

"organization_id": "org_456",

"subscription_id": "sub_789"

}

Другие компоненты могут реагировать на событие независимо от того, какой сервис его создал.

2. Event vs Command vs Message

Event vs Command vs Message — важное различие.

Понятие

Значение

Пример

Event

Факт, который уже произошёл

PaymentCompleted

Command

Запрос выполнить действие

SendInvoiceEmail

Message

Единица передаваемых данных

JSON-сообщение

Например:

PaymentCompleted — событие.

GenerateInvoice — команда.

И то и другое может передаваться через Message Broker.

Но семантика у них разная.

3. Почему события должны описывать прошлое

Domain Events обычно именуют в прошедшем времени.

Например:

UserRegistered

PaymentSucceeded

SubscriptionCanceled

Неудачный вариант:

DoPayment

SendEmailNow

Такие названия больше похожи на команды.

Хорошее имя события помогает понять, какой бизнес-факт произошёл.

4. Что такое Event Producer

Event Producer — компонент, создающий событие.

Например:

Billing Service

|

v

PaymentCompleted

Producer не обязан знать все компоненты, которые будут реагировать на событие.

Это снижает связанность системы.

5. Что такое Event Consumer

Event Consumer — компонент, обрабатывающий событие.

Например:

PaymentCompleted

|

+-- Notification Consumer

|

+-- Analytics Consumer

|

+-- CRM Consumer

Каждый Consumer выполняет собственную задачу.

Если Analytics Service временно недоступен, это не обязательно должно блокировать Billing Service.

6. Что такое Message Broker

Message Broker — инфраструктурный компонент для передачи сообщений между Producers и Consumers.

Популярные решения:

  • RabbitMQ;

  • Apache Kafka;

  • Redis Streams;

  • Amazon SQS;

  • другие системы очередей и потоковой обработки.

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

7. Synchronous vs Asynchronous Communication

Synchronous vs Asynchronous Communication:

Синхронный подход

Асинхронный подход

HTTP Request/Response

Message/Event

Отправитель обычно ждёт ответ

Отправитель может продолжить работу

Проще отслеживать выполнение

Нужна отдельная наблюдаемость

Сильнее связаны времена доступности

Компоненты могут работать независимо

Удобен для немедленного результата

Удобен для фоновых процессов

Не нужно превращать все HTTP-запросы в события.

Например, получение списка проектов через REST API остаётся естественным синхронным сценарием.


Часть 2. Когда нужна Event-Driven Architecture

8. Какие проблемы решает EDA

Benefits of Event-Driven Architecture:

  • уменьшение связности компонентов;

  • асинхронная обработка;

  • независимое масштабирование Consumers;

  • буферизация нагрузки;

  • возможность добавлять новые обработчики;

  • повышение устойчивости к временным отказам.

Например, после регистрации пользователя можно независимо отправлять Welcome Email и обновлять аналитику.

9. Когда EDA не нужна

When Not to Use Event-Driven Architecture:

Событийная архитектура не всегда оправданна.

Для небольшого CRUD-приложения она может создать больше сложности, чем пользы.

Дополнительные затраты включают:

  • инфраструктуру брокера;

  • обработку повторов;

  • мониторинг очередей;

  • управление схемами сообщений;

  • сложную диагностику;

  • согласованность между сервисами.

Если задачу можно надёжно решить внутри одного Database Transaction, не обязательно использовать Broker.

10. Монолит и события

Event-Driven Modular Monolith — подход, при котором приложение остаётся монолитом, но внутренние модули обмениваются событиями.

Например:

FastAPI Monolith

|

+-------------+-------------+

| | |

v v v

Billing Notifications Analytics

| | |

+-------------+-------------+

|

v

Event Bus

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

11. Микросервисы и события

Event-Driven Microservices:

Billing Service

|

v

Event Broker

|

+-- Notification Service

|

+-- Analytics Service

|

+-- Audit Service

Каждый сервис может иметь собственную базу данных.

Но именно здесь особенно важна Eventual Consistency.


Часть 3. RabbitMQ

12. Что такое RabbitMQ

RabbitMQ Message Broker — система передачи сообщений, поддерживающая очереди, Exchanges, Routing Keys и механизмы подтверждения доставки.

Типовая схема:

Producer

|

v

Exchange

|

v

Queue

|

v

Consumer

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

13. Что такое Exchange

RabbitMQ Exchange принимает сообщения от Producers и маршрутизирует их в очереди.

Основные типы:

  • Direct;

  • Topic;

  • Fanout;

  • Headers.

Например, Topic Exchange позволяет маршрутизировать сообщения по Routing Key.

14. Что такое Routing Key

RabbitMQ Routing Key помогает определить, в какие очереди должно попасть сообщение.

Например:

billing.payment.completed

billing.subscription.canceled

user.registered

С помощью Topic Exchange можно подписывать очереди на соответствующие шаблоны.

15. Что такое Queue

RabbitMQ Queue хранит сообщения до их получения и обработки Consumer, в рамках настроенных механизмов надёжности и хранения.

Например:

notification_queue

analytics_queue

crm_sync_queue

Разные очереди могут обрабатываться с разной скоростью.

16. Как организовать Fanout

RabbitMQ Fanout Exchange:

PaymentCompleted

|

v

Fanout Exchange

|

+-----------+-----------+

| | |

v v v

Email Queue Audit Queue Analytics Queue

Каждая связанная очередь получает собственную копию сообщения.

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

17. Что такое Consumer Acknowledgement

RabbitMQ Consumer Acknowledgement — подтверждение обработки сообщения Consumer.

Например:

Consumer receives message

|

v

Process message

|

v

ACK

Если Consumer завершится до подтверждения, сообщение может быть доставлено повторно.

Поэтому Consumer должен быть готов к дубликатам.

18. Что такое Publisher Confirm

RabbitMQ Publisher Confirms позволяют Producer получить подтверждение принятия публикации брокером.

Но подтверждение Broker не означает, что Consumer уже обработал сообщение.

Нужно различать:

  • публикацию сообщения;

  • сохранение сообщения брокером;

  • доставку Consumer;

  • успешную бизнес-обработку.

Это разные этапы.

19. Что такое Dead Letter Queue

RabbitMQ Dead Letter Queue (DLQ) — очередь для сообщений, которые не удалось обработать или которые были перенаправлены по настроенным правилам.

Например:

Main Queue

|

v

Consumer

|

v

Processing Failed

|

v

Retry Policy

|

v

Dead Letter Queue

DLQ помогает анализировать проблемные сообщения.

Но она не заменяет исправление причины ошибок.


Часть 4. Apache Kafka

20. Что такое Apache Kafka

Apache Kafka — распределённая платформа для хранения и обработки потоков событий.

Kafka часто применяется для:

  • Event Streaming;

  • аналитических потоков;

  • интеграции сервисов;

  • обработки больших объёмов событий;

  • хранения истории событий;

  • повторного чтения данных.

21. Kafka Topic

Kafka Topic — логическая категория событий.

Например:

payments

subscriptions

user-events

audit-events

Сообщения внутри Topic распределяются по Partitions.

22. Что такое Kafka Partition

Kafka Partition — упорядоченная последовательность записей внутри Topic.

Например:

Topic: payments

Partition 0:

E1 -> E4 -> E7

Partition 1:

E2 -> E5 -> E8

Partition 2:

E3 -> E6 -> E9

Порядок гарантируется в пределах конкретной Partition при соблюдении соответствующих условий публикации.

Глобального порядка между всеми Partitions нет.

23. Что такое Kafka Offset

Kafka Offset — позиция записи внутри Partition.

Например:

Partition 0

Offset 0 -> Event A

Offset 1 -> Event B

Offset 2 -> Event C

Consumer использует Offsets для отслеживания прогресса чтения.

24. Что такое Consumer Group

Kafka Consumer Group позволяет нескольким Consumers совместно обрабатывать Partitions.

Например:

Topic: payments

|

+-- Partition 0 -> Consumer A

|

+-- Partition 1 -> Consumer B

|

+-- Partition 2 -> Consumer C

В пределах одной Consumer Group Partition обычно назначается одному активному Consumer.

Разные Consumer Groups могут независимо читать один Topic.

25. Kafka Retention

Kafka Event Retention определяет, как долго или в каком объёме Kafka хранит записи.

Например:

Retention:

7 days

Это условный пример.

В отличие от традиционной очереди, прочтение сообщения одним Consumer обычно не удаляет запись из Kafka.

Благодаря этому другие Consumer Groups могут читать те же события.

26. Что такое Event Replay

Kafka Event Replay — повторное чтение ранее сохранённых событий.

Например:

Historical Events

|

v

New Analytics Consumer

|

v

Rebuild Statistics

Это полезно при создании новых аналитических сервисов.

Но повторная обработка требует идемпотентности и контроля побочных эффектов.


Часть 5. Kafka vs RabbitMQ

27. Сравнение Kafka и RabbitMQ

Kafka vs RabbitMQ:

Критерий

RabbitMQ

Kafka

Основная модель

Очереди и маршрутизация

Распределённый журнал событий

Routing

Exchanges и Bindings

Topics и Partitions

Повторное чтение

Зависит от модели и настроек

Естественная часть модели

Хранение истории

Зависит от очередей и конфигурации

Retention

Порядок

Зависит от очереди и Consumers

В пределах Partition

Фоновые задачи

Часто удобен

Возможен, но не всегда оптимален

Event Streaming

Возможен с соответствующими механизмами

Основной сценарий

Сложность эксплуатации

Зависит от требований

Зависит от кластера и нагрузки

Нет универсального победителя.

28. Когда выбрать RabbitMQ

When to Use RabbitMQ:

RabbitMQ часто подходит для:

  • фоновых задач;

  • маршрутизации команд;

  • Email Processing;

  • генерации отчётов;

  • интеграции с Celery;

  • обработки очередей с подтверждениями.

Если основная задача — надёжно передать работу Worker, RabbitMQ может быть удобным выбором.

29. Когда выбрать Kafka

When to Use Kafka:

Kafka часто подходит для:

  • больших потоков событий;

  • независимых Consumer Groups;

  • аналитики;

  • Event Replay;

  • интеграции множества сервисов;

  • долгоживущих потоков данных.

Но не стоит внедрять Kafka только потому, что её используют крупные компании.

30. Redis Pub/Sub vs Redis Streams

Redis Pub/Sub vs Redis Streams:

Redis Pub/Sub подходит для доставки сообщений активным подписчикам.

Но обычный Pub/Sub не обеспечивает долговременное хранение сообщений для отключённых Consumers.

Redis Streams поддерживает хранение записей и Consumer Groups.

Например:

Redis Pub/Sub:

Publish -> Online Subscribers

Redis Streams:

Append -> Stored Stream -> Consumers

Для критических бизнес-событий обычный Redis Pub/Sub без дополнительного механизма надёжности часто недостаточен.


Часть 6. Проблема Dual Write

31. Что такое Dual Write Problem

Dual Write Problem возникает, когда приложение должно изменить базу данных и отправить сообщение в Broker.

Например:

Update PostgreSQL

|

v

Publish Event

Эти действия выполняются в разных системах.

И между ними может произойти сбой.

32. Сценарий потери события

Представим:

1. Update subscription in PostgreSQL

2. COMMIT successful

3. Application crashes

4. Event not published

Подписка активирована.

Но Notification Service не получил событие.

Это приводит к рассогласованию компонентов.

33. Почему публикация до COMMIT тоже опасна

Другой сценарий:

1. Publish PaymentCompleted

2. Consumer receives event

3. Database transaction fails

4. ROLLBACK

Теперь Consumer считает, что платёж завершён.

Но соответствующее изменение в базе не произошло.

Поэтому простой перенос публикации перед COMMIT не решает проблему.


Часть 7. Transactional Outbox Pattern

34. Что такое Transactional Outbox

Transactional Outbox Pattern — архитектурный паттерн, при котором бизнес-изменение и запись события сохраняются в одной транзакции базы данных.

Например:

Database Transaction

|

+-- Update Subscription

|

+-- INSERT Outbox Event

|

v

COMMIT

После COMMIT отдельный Publisher отправляет событие в Broker.

35. Как работает Outbox Pattern

Outbox Pattern Architecture:

FastAPI

|

v

PostgreSQL Transaction

|

+-- Business Table

|

+-- Outbox Table

|

v

COMMIT

|

v

Outbox Publisher

|

v

Message Broker

|

v

Consumers

Это устраняет риск ситуации, когда бизнес-изменение успешно записано, а информация о необходимости публикации события полностью потеряна.

Но Outbox не исключает повторную публикацию.

36. Как создать Outbox Table

PostgreSQL Outbox Table:

CREATE TABLE outbox_events (

id UUID PRIMARY KEY,

event_type TEXT NOT NULL,

aggregate_id UUID NOT NULL,

payload JSONB NOT NULL,

created_at TIMESTAMPTZ

NOT NULL DEFAULT NOW(),

published_at TIMESTAMPTZ,

attempts INTEGER NOT NULL DEFAULT 0

);

CREATE INDEX idx_outbox_pending

ON outbox_events (created_at)

WHERE published_at IS NULL;

aggregate_id может обозначать Subscription, Order, Organization или другую бизнес-сущность.

37. Как сохранить событие в одной транзакции

FastAPI SQLAlchemy Outbox Pattern:

from uuid import uuid4

from sqlalchemy.ext.asyncio import AsyncSession

async def activate_subscription(

session: AsyncSession,

subscription,

):

async with session.begin():

subscription.status = "active"

event = OutboxEvent(

id=uuid4(),

event_type="subscription.activated",

aggregate_id=subscription.id,

payload={

"subscription_id": str(

subscription.id

),

"organization_id": str(

subscription.organization_id

),

},

)

session.add(event)

В примере предполагается, что subscription уже загружена в рамках подходящего контекста и что модель OutboxEvent определена отдельно.

session.begin() управляет транзакцией.

Если запись Outbox не удастся сохранить, бизнес-изменение также не должно быть зафиксировано.

38. Как реализовать Outbox Publisher

Outbox Publisher Worker:

SELECT pending events

|

v

Publish to Broker

|

v

Wait for confirmation

|

v

Mark published

При нескольких Workers необходимо предотвращать конкурентную обработку одной и той же записи.

Для этого можно использовать PostgreSQL FOR UPDATE SKIP LOCKED.

39. Как использовать SKIP LOCKED

PostgreSQL SKIP LOCKED Queue:

SELECT id, event_type, payload

FROM outbox_events

WHERE published_at IS NULL

ORDER BY created_at

FOR UPDATE SKIP LOCKED

LIMIT 100;

Так Workers могут выбирать разные строки, не ожидая блокировок друг друга.

Но нужно учитывать границы транзакций.

Если Worker удерживает транзакцию во время сетевой публикации, это может создавать длительные блокировки.

А если он освобождает строки до публикации, нужен механизм Lease или Claim Status.

40. Почему Outbox не гарантирует Exactly Once

Outbox At-Least-Once Delivery:

Publisher sends event

|

v

Broker confirms

|

v

Publisher crashes

|

v

Event remains unpublished locally

|

v

Publisher sends again

Consumer получает дубликат.

Поэтому Outbox обычно сочетают с Idempotent Consumers.


Часть 8. Idempotent Consumers

41. Что такое Idempotent Consumer

Idempotent Consumer Pattern — обработчик, повторное выполнение которого для одного события не создаёт нежелательных повторных последствий.

Например:

PaymentCompleted

PaymentCompleted

PaymentCompleted

Все три доставки относятся к одному бизнес-событию.

Consumer должен учитывать это.

42. Почему дубликаты неизбежны

At-Least-Once Message Delivery допускает повторную доставку сообщений.

Например:

Consumer processes event

|

v

Database COMMIT

|

v

Consumer crashes before ACK

|

v

Broker redelivers

Поэтому нужно проектировать обработчики с учётом повторов.

43. Как создать Inbox Table

PostgreSQL Inbox Pattern:

CREATE TABLE processed_events (

consumer_name TEXT NOT NULL,

event_id UUID NOT NULL,

processed_at TIMESTAMPTZ

NOT NULL DEFAULT NOW(),

PRIMARY KEY (

consumer_name,

event_id

)

);

Ключ состоит из Consumer Name и Event ID.

Это позволяет нескольким независимым Consumers обрабатывать одно событие.

44. Как реализовать Idempotent Processing

Idempotent Event Processing PostgreSQL:

BEGIN TRANSACTION

|

v

INSERT processed_events

|

+-- Duplicate -> Skip

|

v

Apply Business Change

|

v

COMMIT

|

v

ACK Message

Важно сохранять отметку об обработке и бизнес-изменение в одной транзакции.

Иначе возможна ситуация, когда событие помечено обработанным, но операция не завершена.

45. Пример обработки через SQLAlchemy

SQLAlchemy Idempotent Consumer:

from sqlalchemy.dialects.postgresql import insert

async def process_subscription_event(

session,

event,

):

async with session.begin():

statement = (

insert(ProcessedEvent)

.values(

consumer_name="notification-service",

event_id=event.id,

)

.on_conflict_do_nothing()

.returning(ProcessedEvent.event_id)

)

result = await session.execute(statement)

inserted = result.scalar_one_or_none()

if inserted is None:

return False

notification = Notification(

organization_id=event.organization_id,

event_id=event.id,

type="subscription_activated",

)

session.add(notification)

return True

Это упрощённый пример.

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

Но если Consumer вызывает внешний Email API, необходима дополнительная стратегия идемпотентности внешнего действия.


Часть 9. Delivery Guarantees

46. At-Most-Once Delivery

At-Most-Once Delivery означает, что сообщение может быть доставлено не более одного раза, но возможна потеря.

Например:

Send

|

v

Failure

|

v

No Retry

Подход может быть допустим для некритичной Telemetry.

47. At-Least-Once Delivery

At-Least-Once Delivery предполагает повторные попытки доставки до подтверждения в рамках соответствующей конфигурации и доступности системы.

Это снижает риск потери сообщения, но допускает дубликаты.

Для многих бизнес-процессов такой подход практичен.

48. Exactly-Once Processing

Exactly-Once Processing Semantics — сложная тема.

Некоторые системы предоставляют специальные гарантии обработки в рамках определённых операций.

Но это не означает, что внешние побочные эффекты автоматически выполняются ровно один раз.

Например, отправка Email через сторонний API требует отдельного контроля.

Поэтому в прикладной архитектуре обычно важнее обеспечить идемпотентность бизнес-операций.


Часть 10. Retry и Dead Letter Queue

49. Как организовать Retry

Message Retry Strategy:

Attempt 1

|

v

Failure

|

v

Wait

|

v

Attempt 2

|

v

Failure

|

v

Wait Longer

|

v

Attempt 3

Повторять следует прежде всего временные ошибки.

Например:

  • сетевой Timeout;

  • временная недоступность сервиса;

  • Rate Limit;

  • кратковременная ошибка базы.

50. Что такое Exponential Backoff

Exponential Backoff Retry:

[delay = \min(maxDelay,\ baseDelay \times 2^{attempt})]

Например:

1 second

2 seconds

4 seconds

8 seconds

На практике полезно добавлять Jitter — случайное отклонение задержки.

Это уменьшает риск одновременного повторного обращения большого числа Consumers.

51. Какие ошибки не нужно повторять

Non-Retryable Message Errors:

  • невалидная схема события;

  • неизвестный обязательный тип;

  • отсутствие необходимых данных;

  • постоянная бизнес-ошибка;

  • запрещённая операция.

Повторение таких сообщений без изменения условий не поможет.

Их можно отправлять в DLQ для анализа.

52. Что такое Poison Message

Poison Message — сообщение, которое систематически вызывает ошибку обработки.

Например, Consumer ожидает обязательное поле:

{

"event_type": "payment.completed"

}

Но поле payment_id отсутствует.

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

Поэтому нужны Retry Limits и DLQ.


Часть 11. Eventual Consistency

53. Что такое Eventual Consistency

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

Например:

Billing Database:

Subscription = Active

Analytics Database:

Subscription = Pending

Через некоторое время Analytics Consumer получает событие.

Billing Database:

Subscription = Active

Analytics Database:

Subscription = Active

Такая временная задержка может быть допустимой.

54. Strong Consistency vs Eventual Consistency

Strong Consistency vs Eventual Consistency:

Strong Consistency

Eventual Consistency

Строгие гарантии видимости состояния

Возможны временные расхождения

Удобна для критических локальных операций

Удобна для асинхронной интеграции

Может требовать более тесной координации

Снижает необходимость немедленной координации

Подходит для транзакционных инвариантов

Подходит для производных представлений

Выбор зависит от бизнес-требований.

55. Где Eventual Consistency допустима

Eventual Consistency Examples:

  • обновление аналитики;

  • Email-уведомления;

  • поисковый индекс;

  • синхронизация CRM;

  • генерация отчётов;

  • обновление рекомендаций.

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

56. Где нужна более строгая согласованность

Strong Consistency Business Rules:

Для некоторых операций временное расхождение опасно.

Например:

  • списание денежных средств;

  • изменение доступного баланса;

  • резервирование ограниченного ресурса;

  • проверка уникальности;

  • соблюдение критических лимитов.

Такие инварианты часто следует обеспечивать в одной транзакционной границе.


Часть 12. Saga Pattern

57. Что такое Saga Pattern

Saga Pattern — способ координации бизнес-процесса, состоящего из нескольких локальных транзакций.

Например, создание платной организации может включать:

Create Organization

|

v

Create Subscription

|

v

Provision Resources

|

v

Send Welcome Email

Если один этап не выполнен, может потребоваться компенсирующее действие.

58. Что такое Compensating Transaction

Saga Compensating Transaction — действие, компенсирующее последствия ранее выполненного шага.

Например:

Reserve Resource

|

v

Payment Failed

|

v

Release Resource

Компенсация не всегда является точным техническим откатом.

Например, отправленное Email невозможно «отправить обратно».

Поэтому Saga требует продуманных бизнес-правил.

59. Choreography vs Orchestration

Saga Choreography vs Orchestration:

Choreography

Orchestration

Сервисы реагируют на события

Центральный Coordinator управляет шагами

Меньше центральной логики

Явный Workflow

Может быть сложнее отслеживать процесс

Проще видеть последовательность

Подходит для простых реакций

Подходит для сложных процессов

Оба подхода имеют преимущества и ограничения.

60. Пример Saga для заказа

Order Saga Example:

OrderCreated

|

v

ReserveInventory

|

v

ChargePayment

|

+-- Success -> ConfirmOrder

|

+-- Failure -> ReleaseInventory

В реальной системе нужно учитывать:

  • повторные события;

  • Timeout;

  • неопределённый статус платежа;

  • конкурентные операции;

  • повторные компенсации;

  • восстановление после падения Coordinator.


Часть 13. Event Schema и Versioning

61. Почему событиям нужна схема

Event Schema Design позволяет Consumers понимать структуру сообщений.

Например:

{

"event_id": "evt_123",

"event_type": "subscription.activated",

"event_version": 1,

"occurred_at": "2026-10-30T12:00:00Z",

"data": {

"subscription_id": "sub_456",

"organization_id": "org_789"

}

}

Событие должно иметь понятный контракт.

62. Что такое Event Versioning

Event Schema Versioning — управление изменениями формата событий.

Например:

subscription.activated v1

subscription.activated v2

Новая версия может добавлять поля.

Но удаление или изменение значения существующих полей может нарушить работу Consumers.

63. Backward Compatibility

Backward Compatible Events — события, изменения которых не ломают старые Consumers в рамках согласованного контракта.

Например, добавление необязательного поля часто проще, чем переименование обязательного.

Но совместимость зависит от правил сериализации и поведения Consumers.

64. Schema Registry

Kafka Schema Registry — инструмент управления схемами событий и правилами совместимости.

Часто используется с Avro, Protobuf или JSON Schema.

Для небольшого проекта можно начать с Pydantic Models и контрактных тестов.

65. Как валидировать Event в Pydantic

Pydantic Event Validation:

from datetime import datetime

from uuid import UUID

from pydantic import BaseModel

class SubscriptionActivatedData(BaseModel):

subscription_id: UUID

organization_id: UUID

class SubscriptionActivatedEvent(BaseModel):

event_id: UUID

event_type: str

event_version: int

occurred_at: datetime

data: SubscriptionActivatedData

В Production полезно дополнительно ограничивать допустимые значения event_type и event_version.

Например, через Literal.


Часть 14. Event Ordering

66. Почему порядок событий важен

Event Ordering Guarantees:

Представим:

SubscriptionActivated

SubscriptionCanceled

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

Поэтому для некоторых Aggregate важен порядок событий.

67. Как использовать Aggregate ID

Aggregate-Based Event Ordering:

Aggregate ID:

subscription_123

В Kafka можно использовать Aggregate ID как Message Key.

Это помогает направлять события одного Aggregate в одну Partition при стабильной конфигурации Partitioner.

Но порядок также зависит от поведения Producer и обработки Consumer.

68. Что такое Event Sequence Number

Event Sequence Number:

{

"event_type": "subscription.updated",

"aggregate_id": "sub_123",

"sequence": 42

}

Consumer может сравнивать последовательные номера.

Например:

Current Version: 42

Incoming Version: 41

Action: Ignore stale event

Это помогает предотвращать перезапись нового состояния устаревшими событиями.

69. Как избежать Race Conditions

Event Processing Race Conditions:

Два Consumers могут одновременно пытаться изменить одну запись.

Для защиты можно использовать:

  • Database Transactions;

  • Row-Level Locks;

  • Optimistic Concurrency;

  • Version Columns;

  • Unique Constraints;

  • сериализацию обработки одного Aggregate.

Нужный подход зависит от конкретного бизнес-инварианта.


Часть 15. Observability для событийной архитектуры

70. Какие метрики отслеживать

Event-Driven Architecture Monitoring:

Метрика

Назначение

Published Events

Количество опубликованных событий

Consumer Lag

Отставание Consumers

Processing Duration

Время обработки

Failed Events

Ошибки

Retry Count

Повторные попытки

DLQ Size

Проблемные сообщения

Outbox Age

Задержка публикации

Duplicate Events

Повторные доставки

Особенно полезно отслеживать возраст старейшего необработанного события.

71. Как трассировать события

OpenTelemetry Messaging Tracing:

FastAPI Request

|

v

Create Outbox Event

|

v

Publish Message

|

v

RabbitMQ / Kafka

|

v

Consumer Span

|

v

Database Update

Trace Context можно передавать через Message Headers.

При этом нужно учитывать асинхронную природу обработки и не считать все этапы одной непрерывной синхронной операцией.

72. Как мониторить Outbox

Transactional Outbox Monitoring:

Pending Events

|

v

Oldest Event Age

|

v

Alert if Threshold Exceeded

Если Outbox растёт, возможно:

  • Broker недоступен;

  • Publisher остановлен;

  • публикация слишком медленная;

  • возникла ошибка сериализации;

  • недостаточно Workers.

73. Как мониторить DLQ

Dead Letter Queue Monitoring:

DLQ не должна превращаться в забытое хранилище ошибок.

Нужны:

  • Alerts;

  • классификация причин;

  • инструменты просмотра;

  • безопасный Replay;

  • защита от бесконечных повторов.

Повторный запуск сообщений из DLQ должен учитывать идемпотентность.


Часть 16. Безопасность

74. Как защитить Message Broker

Message Broker Security:

  • Authentication;

  • TLS;

  • Network Isolation;

  • Access Control;

  • Secret Management;

  • Audit Logs;

  • минимальные права Producers и Consumers.

Например, Notification Service не должен иметь право публиковать произвольные финансовые события.

75. Почему нельзя доверять содержимому Event

Event Payload Validation:

Даже если событие пришло из внутреннего Broker, Consumer должен проверять его структуру.

Нельзя считать любое сообщение автоматически корректным.

Особенно важно валидировать:

  • Event Type;

  • Event Version;

  • обязательные поля;

  • идентификаторы;

  • допустимые значения;

  • размер Payload.

76. Как защитить Multi-Tenant Events

Multi-Tenant Event Security:

{

"event_type": "report.generated",

"organization_id": "org_123",

"report_id": "report_456"

}

Consumer должен проверять соответствие Tenant Context.

Нельзя использовать произвольный organization_id без доверенной связи с бизнес-ресурсом.

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


Часть 17. Тестирование

77. Как тестировать Event-Driven Architecture

Event-Driven Architecture Testing должно включать:

  • успешную публикацию;

  • успешную обработку;

  • Duplicate Events;

  • Broker Unavailable;

  • Consumer Crash;

  • Retry;

  • DLQ;

  • Outbox Recovery;

  • неправильный порядок событий;

  • Schema Validation;

  • Tenant Isolation.

78. Как тестировать Outbox

Transactional Outbox Test:

Scenario A:

Business Transaction succeeds

Outbox Event exists

Scenario B:

Business Transaction fails

Outbox Event does not exist

Это один из наиболее важных тестов.

Он подтверждает атомарность локальной транзакции.

79. Как тестировать Idempotent Consumer

Idempotent Consumer Test:

Send Event A

Send Event A again

Send Event A third time

Ожидаемый результат:

One Business Effect

При этом полезно проверять не только количество записей, но и состояние связанных бизнес-сущностей.

80. Как тестировать Consumer Crash

Consumer Failure Recovery Test:

Receive Event

|

v

Apply Database Change

|

v

COMMIT

|

v

Crash Before ACK

|

v

Redelivery

Повторная доставка не должна создавать нежелательные повторные эффекты.

81. Как тестировать Event Ordering

Out-of-Order Event Testing:

Event Version 12

Event Version 11

Ожидаемый результат:

Final Version = 12

Это особенно важно для обновления подписок, заказов и других Aggregate.


Часть 18. Production-архитектура

82. Рекомендуемая Event-Driven Architecture для SaaS

Production Event-Driven Architecture FastAPI PostgreSQL:

React Frontend

|

v

FastAPI Backend

|

v

PostgreSQL

|

+----------+----------+

| |

v v

Business Tables Outbox Events

|

v

Outbox Publisher

|

v

RabbitMQ / Kafka

|

+--------------------+--------------------+

| | |

v v v

Notification Analytics Audit

Consumer Consumer Consumer

| | |

v v v

Inbox Table Inbox Table Inbox Table

| | |

v v v

Business Logic Business Logic Business Logic

|

v

External API

Эта архитектура сочетает несколько механизмов надёжности.

PostgreSQL обеспечивает атомарное сохранение бизнес-изменения и Outbox Event.

Outbox Publisher отправляет события в Broker.

Broker распределяет сообщения между Consumers.

Inbox Pattern помогает предотвращать повторные локальные эффекты.

Retry и DLQ позволяют обрабатывать временные и постоянные ошибки.

Monitoring помогает обнаруживать задержки и отказы.

83. Как внедрять EDA поэтапно

Event-Driven Architecture Implementation Roadmap:

Этап 1. Domain Events

Определить:

  • бизнес-события;

  • Producers;

  • Consumers;

  • Event Schema;

  • Event IDs.

Этап 2. Message Broker

Выбрать:

  • RabbitMQ;

  • Kafka;

  • Redis Streams;

  • другой подходящий инструмент.

Этап 3. Reliability

Добавить:

  • Transactional Outbox;

  • Publisher Confirms;

  • Consumer ACK;

  • Idempotency;

  • Retry;

  • DLQ.

Этап 4. Consistency

Добавить:

  • Aggregate Version;

  • Event Ordering;

  • Reconciliation;

  • Compensating Actions.

Этап 5. Production

Добавить:

  • Monitoring;

  • Tracing;

  • Security;

  • Load Testing;

  • Incident Runbooks.


Часть 19. Типичные ошибки

84. Ошибки Event-Driven Architecture

Первая ошибка — использовать события для каждой операции без реальной необходимости.

Вторая — путать Event и Command.

Третья — считать, что Broker автоматически гарантирует Exactly Once.

Четвёртая — не учитывать Duplicate Events.

Пятая — публиковать события отдельно от Database Transaction без Outbox.

Шестая — считать Publisher Confirm подтверждением бизнес-обработки.

Седьмая — не использовать Consumer ACK.

Восьмая — бесконечно повторять Poison Messages.

Девятая — не создавать DLQ.

Десятая — не мониторить Consumer Lag.

Одиннадцатая — игнорировать Event Ordering.

Двенадцатая — не версионировать Event Schema.

Тринадцатая — использовать Redis Pub/Sub для критических событий без дополнительной надёжности.

Четырнадцатая — не учитывать Eventual Consistency.

Пятнадцатая — создавать слишком большие Event Payloads.

Шестнадцатая — передавать секреты через сообщения.

Семнадцатая — не проверять Tenant Isolation.

Восемнадцатая — не тестировать Consumer Crash.

Девятнадцатая — не отслеживать Outbox Age.

Двадцатая — не документировать бизнес-семантику событий.

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


Часть 20. Production Checklist

85. Чек-лист Event-Driven Architecture

Event-Driven Architecture Production Checklist:

Architecture

  • Определены Domain Events.

  • Разделены Events и Commands.

  • Выбрана подходящая модель Broker.

  • Определены Producers и Consumers.

  • Есть Event Schema.

  • Есть Event Versioning.

  • Определена единица упорядочивания.

Reliability

  • Реализован Transactional Outbox.

  • Есть надёжный Outbox Publisher.

  • Используются подтверждения публикации.

  • Consumers подтверждают обработку.

  • Обработчики идемпотентны.

  • Есть Inbox Pattern или эквивалентная защита.

  • Настроены Retry.

  • Есть Dead Letter Queue.

  • Обрабатываются Poison Messages.

  • Есть Recovery после Crash.

Consistency

  • Определены бизнес-инварианты.

  • Учтена Eventual Consistency.

  • Есть защита от устаревших событий.

  • Используются Aggregate Versions при необходимости.

  • Есть Reconciliation.

  • Для сложных процессов предусмотрены компенсации.

Security

  • Broker защищён.

  • Используется TLS.

  • Настроены Access Controls.

  • Event Payload валидируется.

  • Не передаются секреты.

  • Проверяется Tenant Isolation.

  • Ограничены размеры сообщений.

Monitoring

  • Мониторится Consumer Lag.

  • Мониторится Outbox Age.

  • Мониторится DLQ.

  • Мониторятся Retries.

  • Есть Distributed Tracing.

  • Настроены Alerts.

  • Есть Incident Runbooks.

Testing

  • Проверена успешная доставка.

  • Проверены Duplicate Events.

  • Проверен Broker Failure.

  • Проверен Consumer Crash.

  • Проверены Out-of-Order Events.

  • Проверена Schema Compatibility.

  • Проверена Tenant Isolation.

  • Проверен Recovery.


FAQ

Что такое Event-Driven Architecture?

Архитектурный подход, при котором компоненты системы взаимодействуют через события.

Чем Event отличается от Command?

Event сообщает о произошедшем факте, а Command запрашивает выполнение действия.

Что такое Event Producer?

Компонент, создающий и публикующий события.

Что такое Event Consumer?

Компонент, получающий и обрабатывающий события.

Что такое Message Broker?

Инфраструктурный компонент для передачи сообщений между Producers и Consumers.

Что лучше: Kafka или RabbitMQ?

RabbitMQ часто удобен для очередей задач и маршрутизации сообщений. Kafka особенно полезна для потоков событий, независимых Consumer Groups и повторного чтения истории.

Можно ли использовать RabbitMQ с FastAPI?

Да. FastAPI может публиковать сообщения, а отдельные Workers — обрабатывать их.

Можно ли использовать Kafka с Python?

Да. Существуют Python Clients для работы с Kafka.

Что такое Kafka Topic?

Логическая категория событий в Kafka.

Что такое Kafka Partition?

Упорядоченная последовательность записей внутри Topic.

Что такое Kafka Offset?

Позиция записи внутри Partition.

Что такое Kafka Consumer Group?

Группа Consumers, совместно обрабатывающих Partitions.

Что такое RabbitMQ Exchange?

Компонент RabbitMQ, маршрутизирующий сообщения в очереди.

Что такое RabbitMQ Routing Key?

Ключ, используемый при маршрутизации сообщений.

Что такое Dead Letter Queue?

Очередь для сообщений, которые не удалось обработать в рамках установленной политики.

Что такое Dual Write Problem?

Проблема согласованного изменения базы данных и публикации сообщения в другой системе.

Что такое Transactional Outbox?

Паттерн, при котором бизнес-изменение и событие записываются в одной транзакции базы данных.

Зачем нужен Outbox Publisher?

Чтобы отправлять сохранённые Outbox Events в Message Broker.

Что такое Inbox Pattern?

Механизм учёта обработанных событий для защиты от повторных бизнес-эффектов.

Что такое Idempotent Consumer?

Consumer, повторная обработка одного события которым не приводит к нежелательным повторным последствиям.

Что такое At-Least-Once Delivery?

Модель доставки, допускающая повторное получение сообщения.

Что такое At-Most-Once Delivery?

Модель, при которой сообщение может быть потеряно, но не должно доставляться повторно.

Что такое Exactly-Once Processing?

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

Что такое Eventual Consistency?

Модель, допускающая временное расхождение состояния компонентов с последующим согласованием.

Что такое Saga Pattern?

Подход к координации бизнес-процессов из нескольких локальных транзакций.

Что такое Compensating Transaction?

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

Чем Saga Choreography отличается от Orchestration?

При Choreography сервисы реагируют на события, а при Orchestration процессом управляет Coordinator.

Что такое Event Schema Versioning?

Управление изменениями формата событий.

Что такое Event Replay?

Повторное чтение и обработка сохранённых событий.

Что такое Consumer Lag?

Отставание Consumer от поступающих сообщений или записей.

Что такое Poison Message?

Сообщение, которое систематически вызывает ошибку обработки.

Нужна ли Kafka небольшому SaaS?

Не обязательно. Для небольшого продукта часто достаточно PostgreSQL, Outbox и более простого Broker.

Можно ли использовать EDA в монолите?

Да. Модульный монолит может использовать Domain Events без разделения на микросервисы.


Заключение

Event-Driven Architecture помогает строить приложения, в которых компоненты могут развиваться и масштабироваться независимо.

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

RabbitMQ удобен для многих сценариев очередей задач и маршрутизации сообщений.

Kafka особенно полезна для потоковой обработки событий, хранения истории и независимых Consumer Groups.

Но использование Message Broker само по себе не гарантирует надёжность.

События могут доставляться повторно.

Consumers могут завершаться во время обработки.

Сообщения могут приходить с задержкой или в неправильном порядке.

База данных и Broker могут быть временно недоступны.

Поэтому Production-архитектура должна включать дополнительные механизмы.

Transactional Outbox помогает надёжно связывать бизнес-изменения и публикацию событий.

Idempotent Consumer предотвращает повторные локальные бизнес-эффекты.

Retry и Dead Letter Queue позволяют обрабатывать ошибки.

Event Schema Versioning помогает поддерживать совместимость.

Eventual Consistency позволяет компонентам обновляться асинхронно.

Saga Pattern помогает координировать сложные бизнес-процессы.

Observability позволяет отслеживать задержки, ошибки и состояние очередей.

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

Для небольшого SaaS разумно начать с Domain Events, PostgreSQL Transactional Outbox и одного Message Broker.

По мере роста можно добавлять Kafka, независимые Consumer Groups, Schema Registry, Saga Orchestration и более развитый Monitoring.

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


Наверх