Представим, что вы разрабатываете 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.
Так приложение сможет обрабатывать всё более сложные бизнес-процессы без превращения каждого пользовательского запроса в длинную цепочку зависимостей.