Event-Driven Architecture
Зачем знать: Event-Driven Architecture (EDA) — фундаментальный паттерн для loosely coupled систем. Вместо синхронных request-reply цепочек сервисы публикуют события и реагируют на чужие. EDA решает проблему cascade failures, позволяет fan-out, support multiple consumers, ассинхронные workflows. Middle Go-разработчик должен понимать разницу event vs command, choreography vs orchestration, schema evolution в Kafka, и почему “exactly-once” — это всегда tradeoff.
Содержание
Заголовок раздела «Содержание»- Концепция: EDA, события, команды
- Глубже: choreography vs orchestration, schema evolution
- Gotchas
- Real cases
- Вопросы (25)
- Practice
- Источники
1. Концепция
Заголовок раздела «1. Концепция»1.1 Что такое EDA
Заголовок раздела «1.1 Что такое EDA»Event-Driven Architecture — стиль архитектуры, где компоненты обмениваются информацией через события (events). Producer публикует event, consumers подписываются и реагируют.
[Order Service] ──publishes──→ [Event Stream] │ ├─→ [Inventory Service] (reserves item) ├─→ [Notification Service] (sends email) └─→ [Analytics Service] (records metric)Ключевые свойства:
- Loose coupling: producer не знает consumers (и наоборот).
- Asynchronous: producer не ждёт ответа.
- Fan-out: один event — много consumers.
- Temporal decoupling: consumer может быть offline, обработает позже.
- Easy extension: добавить нового consumer — без изменений producer.
1.2 Event vs Command
Заголовок раздела «1.2 Event vs Command»Event: “что произошло” в прошедшем времени. Statement of fact.
OrderCreated,PaymentReceived,UserRegistered.- Не адресован конкретному получателю.
- Multiple consumers могут реагировать.
- Producer не знает, кто и как обработает.
Command: “что должно произойти” в императиве.
CreateOrder,ProcessPayment,SendEmail.- Адресован конкретному handler.
- Может быть отвергнут (валидация).
- Producer ожидает результат (или ack).
// Eventtype OrderCreatedEvent struct { OrderID string `json:"order_id"` UserID string `json:"user_id"` Total int64 `json:"total"` CreatedAt time.Time `json:"created_at"`}
// Commandtype CreateOrderCommand struct { UserID string `json:"user_id"` Items []Item `json:"items"`}Когда event, когда command:
- Event: что-то случилось, broadcast его.
UserLoggedIn→ analytics, security audit, recommendations. - Command: явное распоряжение.
RefundOrder→ конкретный handler.
1.3 Pub-Sub vs Queue
Заголовок раздела «1.3 Pub-Sub vs Queue»Pub-Sub (publish-subscribe):
- Один message — N consumers (все читают).
- Topics, channels.
- Kafka topic, NATS subject, Redis Pub/Sub, AWS SNS.
[Producer] → [Topic: order-events] ├──→ [Consumer A] ├──→ [Consumer B] └──→ [Consumer C]Queue (point-to-point):
- Один message — один consumer (load-balanced).
- RabbitMQ queue, SQS, NATS queue subscription.
[Producer] → [Queue: tasks] ├──→ [Worker 1] (message X) └──→ [Worker 2] (message Y)Kafka consumer groups — гибрид: внутри группы load balance, между группами fan-out.
1.4 Event-carried state transfer vs Event notification
Заголовок раздела «1.4 Event-carried state transfer vs Event notification»Event notification: “что-то случилось, узнайте детали сами.”
{ "type": "OrderUpdated", "order_id": "123"}Consumer должен сделать API call за деталями: GET /orders/123.
Event-carried state transfer: “что-то случилось, вот full snapshot.”
{ "type": "OrderUpdated", "order_id": "123", "user_id": "u1", "items": [...], "total": 1234, "status": "paid"}Consumer не вызывает back, использует данные из event.
Trade-offs:
- Notification: события маленькие, но coupling через API.
- State transfer: события больше, consumer независим.
Чаще state transfer выигрывает. Loose coupling — главная цель EDA.
1.5 Event Sourcing vs EDA
Заголовок раздела «1.5 Event Sourcing vs EDA»EDA — обмен сообщениями между сервисами. Event Sourcing — хранить state как лог событий (см. отдельный файл).
Они независимы: EDA можно делать без event sourcing (events publishit, но state хранят в БД). Event sourcing — выбор для модели данных.
Часто комбинируют: сервис строит state из событий internally + publishит external events для других сервисов.
1.6 Choreography vs Orchestration
Заголовок раздела «1.6 Choreography vs Orchestration»Два подхода к распределённому workflow.
Choreography: сервисы реагируют на events independently. Нет центрального координатора.
[Order] → OrderCreated event ├─→ [Inventory] → ItemReserved event │ └─→ [Payment] → PaymentCharged event │ └─→ [Shipping] → OrderShipped └─→ [Email] (parallel)Pros: loose coupling, добавить шаг — добавить subscriber. Cons: workflow не виден в одном месте — сложно отслеживать.
Orchestration: центральный orchestrator вызывает сервисы по плану.
[Saga Orchestrator] ├─→ Reserve inventory ├─→ Charge payment (если успех) ├─→ Schedule shipping (если успех) └─→ Compensate (если фейл)Pros: workflow явный, проще debug. Cons: orchestrator — single point of complexity, tight coupling.
Когда что:
- Choreography — для loose, неизвестных конечных consumers.
- Orchestration — для критичных бизнес-процессов (платежи, заказы).
1.7 Message brokers (краткий обзор)
Заголовок раздела «1.7 Message brokers (краткий обзор)»Kafka:
- High throughput (миллионы msg/s per cluster).
- Persistent log, replay.
- Partitioning для scaling.
- At-least-once / exactly-once (transactional).
- Use case: event log, analytics pipeline, microservices integration.
NATS:
- Lightweight, low latency (микросекунды).
- JetStream — persistent layer (alternative to Kafka).
- Subject-based routing с wildcards.
- Use case: real-time microservices, IoT.
RabbitMQ:
- AMQP protocol, flexible routing (exchange types).
- Persistence, ack/nack.
- Slightly slower than Kafka, easier for routing patterns.
- Use case: task queues, RPC patterns.
Redis Streams:
- Lightweight, in Redis.
- Consumer groups.
- Ограниченная persistence, durability vs Kafka.
- Use case: simpler scenarios без отдельного broker.
1.8 Schema evolution
Заголовок раздела «1.8 Schema evolution»События со временем меняются. Producers и consumers деплоятся независимо. Нужны правила backwards/forwards compat.
Backwards compatibility (BC): новый consumer может прочитать старые события. Forwards compatibility (FC): старый consumer может прочитать новые события.
Правила (для Protobuf):
- Не удаляй поля (reserve номер).
- Не меняй тип поля.
- Не меняй tag номера.
- Новые поля — optional.
Avro / Protobuf vs JSON:
- Avro: schema-first, evolution rules built-in.
- Protobuf: бинарный, fast, mature evolution semantics.
- JSON: human-readable, но schema enforcement требует JSON Schema или Cloudevents.
1.9 Schema Registry
Заголовок раздела «1.9 Schema Registry»Confluent Schema Registry, Karapace, AWS Glue Schema Registry.
Функции:
- Хранение схем (по subject = topic).
- Versioning.
- Compat rules (backward, forward, full).
- Producer регистрирует schema перед publish.
- Consumer fetch’ит schema по ID.
Subject naming strategies:
- TopicName:
orders-value(одна схема на topic). - RecordName:
OrderCreatedEvent(схема по типу события). - TopicRecordName:
orders-OrderCreatedEvent(multiple типов в одном topic).
1.10 Delivery semantics
Заголовок раздела «1.10 Delivery semantics»At-most-once: возможна потеря, нет дублей. Producer не ретраит. At-least-once: дубли возможны, потерь нет. Producer retries, consumer должен быть идемпотентен. Exactly-once: дублей нет, потерь нет.
Kafka exactly-once (EOS, exactly-once semantics):
enable.idempotence=true— producer не создаёт дубли при retry.transactional.id— атомарные транзакции producer.isolation.level=read_committed— consumer читает только committed.- Это EOS внутри Kafka. Side effects (HTTP, БД) требуют отдельной идемпотентности.
1.11 Idempotent consumers
Заголовок раздела «1.11 Idempotent consumers»Consumer должен корректно обработать дубли. См. файл 20 (идемпотентность).
Pattern:
func handleOrderCreated(event OrderCreatedEvent) error { // Проверить, не обработано ли уже processed, err := db.IsProcessed(event.EventID) if err != nil { return err } if processed { return nil } // skip
// Выполнить if err := businessLogic(event); err != nil { return err }
// Маркировать processed return db.MarkProcessed(event.EventID)}Pitfall: между business logic и MarkProcessed может упасть. Решение: транзакция (если БД одна) или Outbox pattern.
1.12 Outbox pattern (повтор)
Заголовок раздела «1.12 Outbox pattern (повтор)»См. middle 1 файл по Saga/Outbox. Кратко:
Producer пишет event в одну БД-транзакцию вместе с бизнес-данными. Отдельный worker читает таблицу outbox и публикует в broker. Гарантия — event опубликован тогда и только тогда, когда транзакция commit’нута.
1.13 Event-driven debug challenges
Заголовок раздела «1.13 Event-driven debug challenges»В sync system stack trace показывает весь call chain. В EDA — нет.
Проблемы:
- “Где сообщение?” — может быть в Kafka, может потеряно, может обработано.
- “Какая последовательность?” — Distributed trace без correlation ID — мука.
- “Почему медленно?” — latency скрыта в очередях.
Решения:
- Distributed tracing (OpenTelemetry): trace ID пробрасывается через broker (header).
- Event log search: агрегатор всех событий по correlation ID (Loki, Elastic).
- Event timeline UI: UI для просмотра event flow.
- Dead letter queue (DLQ): failed events для post-mortem.
// Trace ID через Kafka headersmsg := &kafka.Message{ Topic: "orders", Value: payload, Headers: []kafka.Header{ {Key: "trace-id", Value: []byte(spanContext.TraceID().String())}, },}1.14 Trade-offs EDA
Заголовок раздела «1.14 Trade-offs EDA»Pros:
- Scalability — independent consumers.
- Resilience — broker buffers, consumer offline OK.
- Loose coupling — easy extension.
- Audit — events — natural log.
- Multi-language — broker — language-agnostic.
Cons:
- Complexity — debug, monitoring, schema.
- Eventual consistency — sync request-reply гарантирует, async — нет.
- Operational overhead — broker cluster, schema registry.
- Learning curve — команда должна понимать.
- Latency — async добавляет ms-s.
2. Глубже
Заголовок раздела «2. Глубже»2.1 Choreography пример
Заголовок раздела «2.1 Choreography пример»// Order Servicefunc (s *OrderService) CreateOrder(ctx context.Context, req CreateOrderRequest) error { order := &Order{...} if err := s.repo.Save(ctx, order); err != nil { return err } // Outbox publish s.outbox.Publish(ctx, OrderCreatedEvent{ OrderID: order.ID, UserID: order.UserID, Total: order.Total, }) return nil}
// Inventory Servicefunc (s *InventoryService) OnOrderCreated(ctx context.Context, event OrderCreatedEvent) error { if err := s.reserveItems(ctx, event); err != nil { s.publish(InventoryReservationFailed{OrderID: event.OrderID, Reason: err.Error()}) return err } s.publish(InventoryReserved{OrderID: event.OrderID}) return nil}
// Payment Servicefunc (s *PaymentService) OnInventoryReserved(ctx context.Context, event InventoryReserved) error { if err := s.charge(ctx, event); err != nil { s.publish(PaymentFailed{OrderID: event.OrderID}) // compensation: release inventory s.publish(ReleaseInventory{OrderID: event.OrderID}) return err } s.publish(PaymentCharged{OrderID: event.OrderID}) return nil}
// Compensation: Inventory reacts to PaymentFailedfunc (s *InventoryService) OnPaymentFailed(ctx context.Context, event PaymentFailed) error { return s.release(ctx, event.OrderID)}2.2 Orchestration пример
Заголовок раздела «2.2 Orchestration пример»type CreateOrderSagaOrchestrator struct { inventory *InventoryClient payment *PaymentClient shipping *ShippingClient repo *SagaRepository}
func (o *Orchestrator) Execute(ctx context.Context, cmd CreateOrderCommand) error { saga := o.repo.NewSaga(cmd)
// Step 1: Reserve if err := o.inventory.Reserve(ctx, cmd.OrderID, cmd.Items); err != nil { saga.MarkFailed(err) return err } saga.MarkStep("reserved")
// Step 2: Pay if err := o.payment.Charge(ctx, cmd.UserID, cmd.Total); err != nil { // Compensate o.inventory.Release(ctx, cmd.OrderID) saga.MarkFailed(err) return err } saga.MarkStep("paid")
// Step 3: Ship if err := o.shipping.Schedule(ctx, cmd.OrderID, cmd.Address); err != nil { // Compensate in reverse o.payment.Refund(ctx, cmd.OrderID) o.inventory.Release(ctx, cmd.OrderID) saga.MarkFailed(err) return err } saga.MarkStep("shipped") saga.MarkCompleted() return nil}Для production: использовать workflow engines (Temporal, Cadence) которые managae state и retries.
2.3 Temporal как orchestrator
Заголовок раздела «2.3 Temporal как orchestrator»import "go.temporal.io/sdk/workflow"
func CreateOrderWorkflow(ctx workflow.Context, input OrderInput) error { ao := workflow.ActivityOptions{ StartToCloseTimeout: 10 * time.Second, RetryPolicy: &temporal.RetryPolicy{ MaximumAttempts: 3, }, } ctx = workflow.WithActivityOptions(ctx, ao)
if err := workflow.ExecuteActivity(ctx, ReserveInventory, input).Get(ctx, nil); err != nil { return err } defer func() { if /* error happened */ { workflow.ExecuteActivity(ctx, ReleaseInventory, input.OrderID) } }()
if err := workflow.ExecuteActivity(ctx, ChargePayment, input).Get(ctx, nil); err != nil { return err } defer func() { if /* error */ { workflow.ExecuteActivity(ctx, RefundPayment, input.OrderID) } }()
return workflow.ExecuteActivity(ctx, ScheduleShipping, input).Get(ctx, nil)}Temporal handles retries, state, timeouts автоматически. Workflow code — durable.
2.4 Schema registry на практике (Confluent + Go)
Заголовок раздела «2.4 Schema registry на практике (Confluent + Go)»Producer:
import "github.com/riferrei/srclient"
client := srclient.CreateSchemaRegistryClient("http://schema-registry:8081")schema, _ := client.GetLatestSchema("orders-value")
// Encode message with schema IDdata := encodeAvro(payload, schema.Codec())header := []byte{0}binary.BigEndian.PutUint32(idBytes, uint32(schema.ID()))message := append(header, idBytes...)message = append(message, data...)
producer.Produce(&kafka.Message{Value: message})Consumer:
// Read schema ID from first 5 bytesschemaID := binary.BigEndian.Uint32(msg.Value[1:5])schema, _ := client.GetSchema(int(schemaID))payload := decodeAvro(msg.Value[5:], schema.Codec())2.5 Protobuf для events
Заголовок раздела «2.5 Protobuf для events»syntax = "proto3";package events;
message OrderCreated { string order_id = 1; string user_id = 2; int64 total_cents = 3; google.protobuf.Timestamp created_at = 4; repeated Item items = 5;}
message Item { string product_id = 1; int32 quantity = 2;}Migration rules:
- Add field — OK (older consumer ignores).
- Rename — OK (field number matters).
- Remove — reserve number (
reserved 5;). - Type change — break compatibility.
2.6 CloudEvents spec (CNCF)
Заголовок раздела «2.6 CloudEvents spec (CNCF)»Стандартизованный envelope для events. Поддерживается Kafka, NATS, AWS, GCP, Azure.
{ "specversion": "1.0", "type": "com.example.order.created", "source": "/orders", "id": "evt-123", "time": "2026-01-01T00:00:00Z", "datacontenttype": "application/json", "subject": "order/123", "data": { "order_id": "123", "total": 1000 }}Преимущества: cross-platform, цвет single envelope, libraries для many languages.
2.7 Idempotency в consumer practical
Заголовок раздела «2.7 Idempotency в consumer practical»type IdempotentConsumer struct { db *pgxpool.Pool handler func(event Event) error}
func (c *IdempotentConsumer) Handle(ctx context.Context, event Event) error { tx, err := c.db.BeginTx(ctx, pgx.TxOptions{}) if err != nil { return err } defer tx.Rollback(ctx)
// INSERT idempotency, error if duplicate _, err = tx.Exec(ctx, `INSERT INTO processed_events (event_id, processed_at) VALUES ($1, NOW())`, event.ID) if err != nil { if isUniqueViolation(err) { return nil // already processed } return err }
// Business logic в той же транзакции if err := c.handler(event); err != nil { return err }
return tx.Commit(ctx)}Условие: handler работает с той же БД. Если handler делает HTTP call — отдельная idempotency на HTTP уровне.
2.8 Event versioning patterns
Заголовок раздела «2.8 Event versioning patterns»Pattern 1: Versioned topic.
orders.v1,orders.v2. Producer пишет в v2, consumers подписываются на нужную версию.- Pros: чёткое разделение.
- Cons: producers поддерживают обе версии в transition period.
Pattern 2: Versioned schema в одной topic.
- Schema registry хранит несколько версий.
- Consumer fetch’ит schema по ID и upcast’ит.
- Backwards compat обязательна.
Pattern 3: Upcaster.
- Старые события transformируются “на лету” в новый формат.
- Полезно для event sourcing replay.
2.9 Event-driven debugging tactics
Заголовок раздела «2.9 Event-driven debugging tactics»Correlation IDs:
type Envelope struct { EventID string CorrelationID string // одинаковый для всей "цепочки" CausationID string // ID события-причины OccurredAt time.Time Payload any}CorrelationID — для logical chain (saga).
CausationID — для linking события с предыдущим event.
С этим UI может построить full timeline.
Distributed tracing через broker:
- Producer добавляет
traceparentheader (W3C). - Consumer extract’ит и продолжает trace.
- OpenTelemetry instrumentation для Kafka, NATS — есть.
2.10 Dead Letter Queue (DLQ)
Заголовок раздела «2.10 Dead Letter Queue (DLQ)»Если consumer не может обработать event (после N retries) — отправить в DLQ.
[Main Topic] → [Consumer] (3 attempts fails) ↓ [DLQ: orders-dlq] ↓ [Manual / Replay process]Pattern:
- N retries с exponential backoff.
- После N → publish в DLQ topic.
- Включить original event + error context.
- Operator анализирует и решает: replay, fix, drop.
func handle(ctx context.Context, event Event) error { for attempt := 1; attempt <= 3; attempt++ { if err := process(event); err != nil { time.Sleep(time.Duration(attempt) * time.Second) continue } return nil } return publishToDLQ(event, lastErr)}3. Gotchas
Заголовок раздела «3. Gotchas»⚠️ Event != Command. Если ваш “event” имеет single consumer и явный intent — это command. Smell: SendEmailEvent (это command).
⚠️ At-least-once = duplicate events. Каждый consumer должен быть идемпотентен. Tracking processed_event_ids.
⚠️ Exactly-once в Kafka — внутри Kafka. HTTP calls, БД записи в другие системы — не покрыты. Уделяй внимание side effects.
⚠️ Out-of-order delivery. Kafka гарантирует order только внутри partition. Между partitions order не гарантирован. Используй partition key (например, orderID).
⚠️ Schema breaking changes. Producer выкатил new schema без backwards compat → consumer не парсит. Schema registry с compat rules — обязательно.
⚠️ Event-carried state too big. Если event 10MB — broker заваливается. Используй reference (notification + API call) или CDC patterns.
⚠️ PII в events. Sensitive данные в Kafka — compliance (GDPR). Encryption at rest, ACLs на topics, audit logs.
⚠️ Consumer lag. Если consumer медленный — events накапливаются. Monitoring lag (Burrow для Kafka). Auto-scaling consumers.
⚠️ Rebalance storms. В Kafka consumer group часто rebalance → паузы. Stable consumers с правильным session.timeout.
⚠️ Choreography → “spaghetti.” Без документации event flow трудно отследить. Используй event maps, BPMN.
⚠️ Orchestrator monolith. Большой orchestrator сам становится monolith. Разбивай на бизнес-процессы.
⚠️ Dead letter ad infinitum. Если DLQ не мониторится — теряются события. Alert на DLQ size.
⚠️ Eventually consistent UX. “Заказ создан, но не показывается в списке секунду.” Учитывай в UI: оптимистический update, polling.
⚠️ Replay events опасно. Если consumer не идемпотентен — replay создаст дубли (вторые charges, double emails). Toggle “replay mode” или dry-run.
⚠️ Compaction в Kafka. Compacted topic хранит latest value per key. Полезно для state, но не для events. Не путай.
⚠️ Tombstones. Для удаления в compacted topic — null value. Consumer должен handle null.
⚠️ Distributed transaction is hard. Saga — eventually consistent, не atomic. Если требуется atomicity — переосмысли границы сервиса.
4. Real cases
Заголовок раздела «4. Real cases»Uber — fast ETA с Kafka. Trip events → Kafka. Многочисленные consumers: ETA prediction (ML), pricing, analytics, fraud detection. Petabytes/day, миллионы msg/s.
Netflix — event-driven re-architecture. Когда переходили с DVD-by-mail на streaming, событийная архитектура помогла. View events, recommendations pipeline, A/B test events.
Stripe — event-driven webhooks.
External events для customers. Stripe гарантирует at-least-once delivery. Customers обязаны делать idempotent handlers. Stripe-Signature header для verification.
LinkedIn — Kafka birth. Kafka рождён в LinkedIn 2011 для unification log/event pipelines. Сейчас open source CNCF.
Shopify — Kafka для inventory updates. Когда заказ → inventory event → multiple downstream (search, recommendations, warehouses). EDA для loose coupling.
Yandex.Eats — order pipeline. Order создан → publish в Kafka. Consumers: courier matching, restaurant POS, ETA calculation, customer notifications. Independent scaling каждого.
Tinkoff — payment processing. Платёжные события через Kafka. Outbox pattern в каждом сервисе. Idempotent consumers.
Cloudflare — analytics pipeline. Edge events (миллиарды/день) → Kafka → ClickHouse. Real-time analytics, abuse detection.
Discord — message delivery. Cassandra для storage + Kafka для real-time fanout (миллионы concurrent users в гильдиях).
Slack — real-time messaging. Messages через broker → fan-out к connected clients. Edge servers с WebSocket connections.
Avito — search indexing. Listing changes → event → search reindex (Elasticsearch). Decoupled: листинг и search независимы.
Mercari — eventual consistency в каталоге. Item posted → event → search index update (eventual). UX optimistic update.
Kafka Streams in Confluent customers. Real-time aggregations, joins, windowing. Используется banks (fraud detection), retailers (real-time inventory).
NATS at Synadia / Adidas. Adidas использует NATS для глобального real-time pricing/inventory. NATS JetStream для durability.
Saga orchestrator с Temporal: Coinbase, Stripe, Snap используют Temporal для workflows. Order processing, payment retries, KYC pipelines.
5. Вопросы
Заголовок раздела «5. Вопросы»Q1: Что такое Event-Driven Architecture? A: Архитектурный стиль, где компоненты обмениваются через события. Loose coupling, async, fan-out, temporal decoupling. Producer не знает consumers.
Q2: Event vs Command — в чём разница? A: Event — “что произошло” (past), нет адресата, multiple consumers. Command — “что должно произойти” (imperative), конкретный handler, может быть отвергнут.
Q3: Pub-Sub vs Queue — когда что? A: Pub-Sub — один message всем consumers (broadcast). Queue — один message одному worker (load balance). Kafka consumer groups — гибрид.
Q4: Event notification vs event-carried state transfer? A: Notification — small event, consumer вызывает API за деталями. State transfer — full data в event. State transfer чаще выигрывает (loose coupling).
Q5: EDA vs Event Sourcing — это одно и то же? A: Нет. EDA — обмен сообщениями. Event Sourcing — хранение state как event log. Можно комбинировать, но независимы.
Q6: Choreography vs Orchestration — в чём разница? A: Choreography — сервисы реагируют independently на events, нет координатора. Orchestration — центральный orchestrator вызывает по плану.
Q7: Когда выбрать choreography? A: Loose coupling, unknown consumers, простой workflow. Добавить шаг — добавить subscriber.
Q8: Когда выбрать orchestration? A: Сложный workflow, нужна видимость прогресса, явные compensations. Critical business processes (платежи, заказы).
Q9: Какие message brokers популярны? A: Kafka (high throughput, persistent log), NATS (low latency, JetStream), RabbitMQ (flexible routing AMQP), Redis Streams (lightweight).
Q10: At-least-once vs exactly-once? A: At-least-once — дубли возможны, потерь нет. Exactly-once — без дублей и потерь. Exactly-once в Kafka — внутри Kafka (transactional). Side effects требуют отдельной идемпотентности.
Q11: Как сделать consumer идемпотентным? A: Хранить processed_event_ids. Перед обработкой проверять. Wrap всё в transaction если возможно. Или Outbox pattern.
Q12: Что такое Outbox pattern? A: События пишутся в одну транзакцию с бизнес-данными. Отдельный worker читает таблицу outbox и публикует в broker. Гарантия atomicity.
Q13: Schema evolution в EDA — какие правила? A: Backwards compat (new consumer reads old). Forward compat (old consumer reads new). Не удаляй поля, не меняй типы, новые — optional.
Q14: Что такое Schema Registry? A: Сервис хранения и versioning схем (Confluent, Karapace). Compat rules (backward, forward, full). Producer регистрирует, consumer fetch’ит по ID.
Q15: Avro vs Protobuf vs JSON для events? A: Avro — schema-first, evolution built-in. Protobuf — бинарный, быстрый, mature evolution. JSON — human-readable, нужен JSON Schema/Cloudevents для контроля.
Q16: Что такое CloudEvents? A: CNCF спецификация envelope для events. Поля: type, source, id, time, data. Cross-platform standard.
Q17: Что такое Dead Letter Queue? A: Topic для failed events после N retries. Operator анализирует, replay или fix. Защита от blocking main pipeline.
Q18: Как делать distributed tracing в EDA? A: OpenTelemetry: trace ID в headers (Kafka headers, RabbitMQ headers). Consumer extract’ит и продолжает span.
Q19: Что такое correlation ID и causation ID? A: Correlation ID — единый для logical chain (saga). Causation ID — ID события-причины. Помогают reconstruct event timeline.
Q20: Какие проблемы у EDA? A: Eventual consistency, complexity debug, schema evolution, operational overhead (broker cluster), learning curve, ordering guarantees.
Q21: Как обеспечить order в Kafka? A: Только внутри partition. Используй partition key (orderID) — все события для одного order в одну partition.
Q22: Что такое compaction в Kafka? A: Topic хранит только последнее value per key (вместо всех). Полезно для state snapshots, не для event log.
Q23: Дубли событий — нормально? A: В at-least-once — да, нормально. Consumer должен быть идемпотентен. В at-most-once — нет дублей, но потери возможны.
Q24: Как тестировать EDA? A: Unit (handler логика), integration (с реальным broker, например testcontainers), contract test (producer-consumer schema), E2E через event flow.
Q25: Сколько событий в секунду может обработать Kafka? A: Производительность зависит от размера, кол-ва partitions, hardware. Уровень: миллионы msg/s на cluster (3-10 nodes), single partition — десятки тыс. msg/s.
6. Practice
Заголовок раздела «6. Practice»-
Реализуй pub-sub on Kafka. Producer публикует OrderCreated, 2 consumers (Inventory, Notification) обрабатывают независимо.
-
Choreography saga. 3 сервиса (Order, Inventory, Payment) реагируют на events. Симулируй PaymentFailed → Compensation (Release inventory).
-
Orchestration saga с Temporal. Тот же workflow, но через Temporal SDK. Запусти Temporal locally (docker-compose).
-
Idempotent consumer. Storage processed_event_ids в Postgres. Симулируй duplicate delivery, покажи отсутствие side effects.
-
Schema registry на практике. Confluent Schema Registry в docker. Avro schema для OrderCreated. Producer регистрирует, consumer decodes.
-
Backwards compat test. v1 schema → publish 100 events. Deploy v2 (с extra field). Consumer на v1 schema должен по-прежнему читать.
-
DLQ implementation. Consumer с 3 retries, потом publish в DLQ topic. Отдельный consumer DLQ — для inspection.
-
Distributed tracing. OpenTelemetry с Kafka. Trace ID пробрасывается через headers. Jaeger UI показывает чейн.
-
Outbox pattern. Transactional outbox table. Worker debezium-like читает changes и publishит в Kafka.
-
Compare brokers. Реализуй one and the same scenario в Kafka, NATS, RabbitMQ. Сравни latency, throughput, complexity.
7. Источники
Заголовок раздела «7. Источники»- Martin Fowler. “What do you mean by ‘Event-Driven’?” 2017. https://martinfowler.com/articles/201701-event-driven.html
- Chris Richardson. “Microservices Patterns.” Manning, 2018. — главы Saga, Outbox.
- Confluent. “Kafka: The Definitive Guide.” 2nd ed. — Gwen Shapira et al.
- Greg Young. “CQRS Documents.” (event sourcing foundations).
- CloudEvents specification. https://cloudevents.io/
- Confluent Schema Registry docs. https://docs.confluent.io/platform/current/schema-registry/
- Temporal documentation. https://docs.temporal.io/
- NATS documentation. https://docs.nats.io/
- Eventuate Tram framework. https://eventuate.io/
- Uber Engineering. “Designing Schemaless.” 2016. https://www.uber.com/blog/schemaless-part-one-mysql-datastore/
- LinkedIn. “Apache Kafka, Samza, and the Unix Philosophy of Distributed Data.” 2014.
- Vaughn Vernon. “Reactive Messaging Patterns with the Actor Model.” 2015.