Event-driven архитектура: события и брокер
События vs команды, выбор брокера, schema evolution, ordering guarantees, replay для recovery — без распределённого ада.
Спроектируй event-driven архитектуру для {{domain}}. Масштаб: {{scale}}.
Базовая аксиома: event-driven — не silver bullet. Это контракт «producer ничего не знает о consumer'ах», цена — eventual consistency, debugging через несколько систем, schema versioning навсегда.
1. События vs команды — НЕ путать
| Атрибут | Event | Command |
|---|---|---|
| Смысл | «что-то случилось» (past tense) | «сделай это» (imperative) |
| Имя | OrderPlaced, PaymentFailed | PlaceOrder, RetryPayment |
| Адресат | broadcast, 0..N подписчиков | один обработчик |
| Ответственность | producer не знает кто слушает | sender знает receiver'а |
| Когда отвергнуть | никогда (факт уже случился) | можно (validation, authz) |
Событие — это факт о прошлом. Если producer ожидает что consumer сделает X — это команда замаскированная под event. Это создаёт скрытую связанность.
2. Когда event-driven уместно
Подходит:
- Несколько consumer'ов на один факт (analytics + email + audit)
- Domain events для CQRS / event sourcing
- Слабая связанность доменов (orders ↔ inventory)
- Async обработка с retry семантикой
- Replay для backfill / recovery
Не подходит:
- Request/response с low latency (HTTP проще)
- Strong consistency (transfer money — sync, в одной транзакции)
- 2 сервиса и 3 события — лишний overhead
3. Выбор брокера
| Брокер | Сильные стороны | Слабые |
|---|---|---|
| Kafka | High throughput, retention days/weeks, ordering per partition, replay | Operational complexity, нужна команда |
| RabbitMQ | Гибкий routing, низкая latency, work queues | Не для replay, retention короткий |
| SNS + SQS | Managed, fan-out из коробки, дёшев на старте | Нет ordering без FIFO, нет replay |
| NATS / NATS JetStream | Lightweight, low-latency, JetStream даёт persistence | Меньше ecosystem |
| Pulsar | Tiered storage, geo-replication, multi-tenancy | Сложнее Kafka, меньше community |
| EventBridge | AWS-native, schema registry, source routing | Vendor lock, лимиты |
Эвристика:
- Стартап, fan-out, дёшево → SNS+SQS / EventBridge
- Event sourcing, replay, high throughput → Kafka / Pulsar
- Сложный routing, work queues → RabbitMQ
- Microservices в k8s без AWS lock → NATS JetStream
«Возьмём Kafka на вырост» — самая частая ошибка. Это полноценная распределённая БД с ops cost.
4. Schema evolution — главное
Event живёт годами в логе. Через 2 года нужно прочитать старые события — schema должна это позволить.
Backward compatible (новый consumer читает старые события):
- Добавление поля с default
- Расширение enum (если consumer'ы fallback'ят)
- Optional поля
Forward compatible (старый consumer читает новые события):
- Игнорировать unknown поля
- Не делать required новые поля
Breaking changes:
- Переименовать / удалить поле
- Изменить тип
- Сузить значения
Для breaking — версионируй: OrderPlacedV2, отдельный topic / event type. Старый поток поддерживай пока есть consumer'ы.
{
"eventType": "OrderPlaced",
"schemaVersion": "2.1.0",
"eventId": "uuid",
"occurredAt": "2026-05-17T12:00:00Z",
"data": { ... }
}
Schema registry (Confluent, AWS Glue, Apicurio):
- Регистрирует схемы (Avro / Protobuf / JSON Schema)
- Compatibility check на publish — нельзя сломать
- Consumer fetches schema по ID
Без registry — schema живёт в репозитории producer'а; через год никто не знает что в payload.
5. Ordering guarantees
| Уровень | Что гарантирует | Цена |
|---|---|---|
| Global ordering | Все события в общем порядке | Single partition, ~МБ/s throughput max |
| Per-partition | Порядок внутри partition | Partition key выбран правильно |
| Per-entity (по key) | События одного aggregate в порядке | Стандарт |
| No ordering | Любой порядок | Max throughput, требует idempotency |
Главное: ordering = partitioning. partitionKey = orderId → все события одного заказа в одной partition → в порядке.
Подводные камни:
- Re-partitioning при scale up = re-ordering
- Slow consumer на одной partition блокирует только её, но lag растёт
- Cross-aggregate ordering — нет (orders и payments независимы)
6. Replay
Replay — суперсила event-driven. Кейсы:
- Bug в consumer — пересчитать с какого-то offset
- Backfill для нового consumer (analytics задним числом)
- Disaster recovery — пересоздать read model из лога
Что нужно:
- Retention достаточный (минимум 7 дней, лучше 30+, для event sourcing — forever)
- Consumer идемпотентен (replay = дубль каждого события)
- Tooling: «replay topic X с offset Y до Z для consumer group G»
Анти-паттерн: short retention + non-idempotent consumer = replay невозможен.
7. Идемпотентность consumer'а
Replay и at-least-once дают дубли. Consumer обязан быть идемпотентным.
Способы:
- Dedup table:
processed_events(event_id PRIMARY KEY, processed_at). Перед обработкой — INSERT IGNORE, если конфликт — skip - Idempotent operation:
UPSERTпо natural key, set вместо increment - Versioning:
UPDATE ... WHERE version = ?— optimistic concurrency
Без идемпотентности replay уничтожит данные.
8. Event sourcing — отдельный зверь
Event-driven ≠ event sourcing.
- Event-driven: события — способ коммуникации
- Event sourcing: события — источник истины, state derived из них
Event sourcing берёт когда:
- Audit / compliance требует полной истории
- Domain reasoning natural в терминах событий (banking, trading)
- Нужна возможность пересчитать state с разными правилами
Не берёт «потому что cool» — debugging сложнее, queries дороже, миграция данных = миграция логики проекции.
9. CQRS уместно?
Command-Query Responsibility Segregation: read и write модели разные.
Хорошо когда:
- Read и write имеют сильно разный shape (write — нормализован, read — denormalized для UI)
- Read throughput >> write throughput
- Несколько read models на один write
Цена:
- Eventual consistency между write и read
- Дополнительная инфра (projection, sync)
- Сложнее transactional «прочитал-обновил»
Не CQRS:
- CRUD app
- Read и write почти одинаковые
- Не нужна eventual consistency
10. Observability event-driven
Без неё — слепой полёт. Минимум:
- Trace context в каждом событии (W3C traceparent в header) — иначе не свяжешь end-to-end
- Event lag per topic per consumer group
- Schema mismatch rate (consumer не смог распарсить)
- DLQ inflow (события которые consumer не смог обработать)
- Throughput producer / consumer
- Time-to-process (occurredAt → consumer ack)
Алерты:
- Lag > threshold
- DLQ inflow > 0
- Schema parse failure > 0
- Consumer group rebalance loop
11. Контракты и тесты
- Schema registry compatibility check на publish — CI gate
- Contract tests: producer публикует sample → consumer парсит. Pact или просто snapshot
- Replay test в staging: «прогони последние 24h в новый consumer, проверь что не упал»
Анти-паттерны
- ❌ Event как замаскированная команда (
SendEmailevent с одним receiver — это команда) - ❌ Schema без версии — через год кошмар миграции
- ❌ Schema живёт в producer репо — consumer узнаёт что сломали в production
- ❌ Required новые поля — старые consumer'ы падают
- ❌
partitionKey = random— нет ordering, нет smart routing - ❌ Retention 1 день — replay невозможен
- ❌ Consumer не идемпотентен — at-least-once + retry = corrupted state
- ❌ «Возьмём Kafka» в стартапе с 10 events/sec
- ❌ Event sourcing «потому что моден» — debugging боль навсегда
- ❌ Cross-aggregate transaction через события — eventual consistency лишает атомарности
- ❌ DLQ без owner и monitoring — silent data loss
В конце
- Список event'ов (имя в past tense, версия, schema)
- Брокер и почему этот
- Partitioning strategy и ordering guarantee
- Schema registry + compatibility policy
- Retention + replay capability
- Consumer idempotency mechanism
- Observability (lag, DLQ, traces)
- Контрактные тесты в CI
Таксономия событий
Названия событий и параметров так, чтобы аналитик через год не плакал.
Multi-agent: координатор и специалисты
Архитектура из координатора и специализированных агентов: передача контекста, дедупликация, race conditions.
Новый subagent или новый skill: что выбрать
Decision tree: создавать ли отдельного агента или достаточно skill. Критерии — контекст, переиспользование, frequency, complexity.