Skip to content
PПромтбук
RUEN
04Архитектура

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 команды — НЕ путать

АтрибутEventCommand
Смысл«что-то случилось» (past tense)«сделай это» (imperative)
ИмяOrderPlaced, PaymentFailedPlaceOrder, 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. Выбор брокера

БрокерСильные стороныСлабые
KafkaHigh throughput, retention days/weeks, ordering per partition, replayOperational complexity, нужна команда
RabbitMQГибкий routing, низкая latency, work queuesНе для replay, retention короткий
SNS + SQSManaged, fan-out из коробки, дёшев на стартеНет ordering без FIFO, нет replay
NATS / NATS JetStreamLightweight, low-latency, JetStream даёт persistenceМеньше ecosystem
PulsarTiered storage, geo-replication, multi-tenancyСложнее Kafka, меньше community
EventBridgeAWS-native, schema registry, source routingVendor 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Порядок внутри partitionPartition 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 как замаскированная команда (SendEmail event с одним 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
К подразделу «Архитектура»
Похожие промты