The 5 Traps of Event-Driven Architecture
System Design Lab · 2026-05-16 · 11м 2с · 245 просмотров · YouTube ↗
Топики: durable-execution
🎧 Аудио
📝 Summary
model=deepseek-v4-flash · prompt=summary-v7 · 3 793→2 295 tokens · 2026-07-20 15:09:40
🎯 Главная суть
Событийно-ориентированная архитектура (EDA) выглядит на схеме как идеально развязанные сервисы, но за каждым событийным потоком прячутся пять системных ловушек: идемпотентность, порядок, обработка сбоев, атомарность двойной записи и управление контрактами. Игнорирование этих ловушек приводит к задвоенным заказам, потерянным событиям, мёртвым очередям-кладбищам и рассинхронизации данных. EDA стоит применять только при реальной необходимости массовой рассылки (фан-аут), и только если команда готова к операционной зрелости — иначе это просто косвенный вызов функции с лишними задержками.
Ловушка 1: доставка ≠ бизнес-корректность
Простое событие «пользователь заплатил $200» приходит в потребителя. Тот списывает с баланса: было $1000, стало $800. Но перед подтверждением (ack) брокеру потребитель падает. Брокер повторяет — новый экземпляр потребителя обрабатывает то же событие, списывает ещё раз, баланс становится $600. Пользователь получил двойное списание. Kafka обещает exactly-once, но только внутри самого Kafka; как только данные уходят в Postgres или внешний API, гарантия исчезает. Решение — идемпотентность: каждое событие получает стабильный ID, потребитель перед выполнением проверяет таблицу обработанных сообщений. Если ID уже есть — событие отбрасывается. Если нет — выполняет работу и записывает ID в той же транзакции. Повторы становятся безвредными.
Ловушка 2: иллюзия порядка
Пользователь сначала нажимает «заказать», а через секунду «отменить». Два события отправлены. Если очередь не гарантирует порядок (SQS) или используется всего одна партиция Kafka, порядок может нарушиться. В худшем случае «отмена» проходит мгновенно, а «заказ» задерживается из-за ретрая. «Отмена» приходит первой — потребитель не находит заказа и выбрасывает ошибку. Затем приходит «заказ» — в системе появляется призрачный ордер, который уже должен быть мёртв. Решение — каждое событие снабжается версией. Потребитель отслеживает последнюю версию для каждой сущности и игнорирует старые события, пришедшие после новых.
Ловушка 3: DLQ как кладбище без присмотра
Dead Letter Queue (DLQ) часто воспринимают как готовое решение: отправить туда упавшие сообщения и забыть. Но каждое сообщение в DLQ — это необработанное бизнес-событие: неподтверждённая регистрация, неотправленный email, неснятая оплата. Особенно опасны «отравленные» сообщения, которые не удаётся распарсить. Например, политика повторных попыток SNS по умолчанию: 3 мгновенных, 2 с задержкой 1 секунда, затем 10 экспоненциальных. В итоге одно испорченное событие ретраится более 100 000 раз на протяжении 23 дней, заглушая все легитимные алерты. Эффективное управление DLQ требует трёх вещей с первого дня: CloudWatch-алерты по глубине очереди (любое сообщение выше нуля будит человека), runbook с инструкцией (переотправить или отбросить), и еженедельная ревизия очереди. Без этого DLQ — просто кладбище.
Ловушка 4: dual-write — проблема двух записей
Большинство продюсеров делают два действия: сохраняют что-то в базу данных, а затем публикуют событие. Между этими двумя вызовами нет транзакции — это окно уязвимости. Пример: продюсер сохранил заказ в свою БД, затем вызвал брокера для публикации события, но между этими двумя вызовами упал. Заказ есть в БД, события нет. Сервисы, подписанные на это событие, ничего не знают о заказе. Если поменять порядок (сначала публикация, потом запись) — сценарий обратный: событие ушло, но запись не состоялась. Решение — транзакционный outbox: в рамках одной транзакции БД записываются и бизнес-строка, и строка в таблицу outbox. Отдельный релей (DynamoDB Streams, Postgres CDC, EventBridge pipes) читает outbox и публикует события. Продюсер никогда не обращается к брокеру напрямую. Если релей падает, он восстанавливается с места остановки — публикация становится такой же надёжной, как запись в БД.
Ловушка 5: саги и схемы — две стороны одной проблемы
Бизнес-процесс редко состоит из одного события — это цепочка: заказать, списать деньги, зарезервировать на складе, отгрузить. Если на шаге 3 произошёл сбой, нельзя просто откатить транзакцию — нужно выполнить компенсирующие события: вернуть платеж, освободить резерв. Пока это разворачивается, пользователь видит статус «в обработке» — ошибка синхронизации превращается в UX-проблему. Вторая сторона: схемы событий эволюционируют. Через полгода команда, владеющая продюсером, переименовывает поле customerId в userId, изменения проходят код-ревью и интеграционные тесты, но контракт, который живёт только в трёх других сервисах, ломается. Все потребители падают. Решение — schema registry: каждая публикация проверяется на соответствие контракту. Переименования, удаления, смена типов отклоняются на этапе публикации, а не в 3 часа ночи. Правило: добавлять необязательные поля, никогда не переименовывать и не удалять. При нарушении совместимости — версионировать. Каждое событие — это публичный API.
Шесть обязательных условий перед внедрением EDA
EDA стоит применять только когда фан-аут реален — пять команд нуждаются в одном событии, рабочие процессы асинхронны по природе. Если событие читает всего один потребитель, вы не строите EDA, а получаете косвенный вызов функции с лишней задержкой и худшими логами. Перед внедрением должны быть готовы шесть компонентов: (1) идемпотентные потребители со стабильным ID события; (2) schema registry для проверки каждого публикации; (3) мониторинг DLQ с реальными алертами (не просто очередь, а глаза на ней); (4) сквозные correlation IDs через все сервисы; (5) транзакционный outbox на каждом продюсере; (6) продуктовая команда, способная объяснить пользователям eventual consistency не кривя душой. Операционная зрелость идёт первой, архитектура — второй.
📜 Transcript
en · 1 472 слов · 25 сегментов · clean
Показать текст транскрипта
Monday morning, sprint planning. Someone walks up to the whiteboard and draws three boxes. A producer, a broker, and a consumer. Decoupled architecture. The cleanest thing you will see today. You scale every service on its own, and events... flow between them. One team owns the order service, another own notifications, and the third one owns analytics. Nobody steps on each other. Your retention slide says modern event-driven architecture. Your investors love it. Your engineers love it. The whiteboard also love it. But here's what the whiteboard doesn't tell you. Behind every one of these arrows, there's five lies. Delivery doesn't mean the work is done. Order is not guaranteed. Failures, Don't surface anywhere visible. Two rights look atomic but they aren't and the schema you ship today will not be the schema you ship the next quarter. Five hidden traps, every event-driven system has them. The lucky teams find them on a Friday afternoon, the rest find them at 3 a.m. The first lie, delivery does not equal business correctness. Now think about something simpler, a user paid for something. just 200 the event fires lands at your consumer the work gets done or you think so here's the actual story account debted 200 the consumer picks it up updates the database balance drop it from one thousand dollar down to eight hundred dollar right before it sends the acknowledge back to the broker the consumer crashes server dies pod restarts now for the broker's point of view the work never happened no act came back So it does what it's designed to do it retries a fresh consumer instance pick up the same message update the database again now balance is drop it to $600 the user just got charged twice for one purchase delivery work it business is broken Now Kafka users will say their broker has exactly one semantic and they are right But they are right inside Kafka only. The moment you write to Postgres or call an external API, that promise dies. The fix is item potency. Every event gets a stable ID. Before doing the work, the consumer check a processed message table. ID already there, drop the event. It's not there, do the work. Write the ID. in the same transaction. Retries become harmless. The first one debts, the duplicate sees itself, gets rejected and disappears. Idempotency is not optional. It's the price of admission to event-driven system. If your consumer cannot handle being called twice, it cannot handle production. Lie number two, the ordering illusion. Now picture a user clicking place order and a second later clicking cancel. Two event fires into your queue. Same order coming out. But there's a spoiler. SQS make no ordering promises. Kafka does, but only inside a single partition. So order placed, varied first. Half across, it hints a transit error. Goes into a retry loop. Order cancelled walks right past it. Order cancelled land first. The consumer says, cancel what? There's no order yet. And it throws an error. A few seconds later, order placed finally arrives. And you have got a ghost order in your system that should already be dead. You cannot ask the broker to fix this. You design for it. Every event gets a version. The consumer track the latest version per entity. When an older event walks in after a newer one, you drop it. Ordering is your problem, not the broker. Lie number three. DLQs and poisons messages. Now let's talk about what happens when things actually go wrong. You have heard of the dead letter Q. Most teams ship one and call it a day. That's where the trouble starts. A DLQ isn't a solution, it's an isolation chamber. And every single message inside of it represents an unprocessed business event. A user who signed up but never got a welcome email. An invoice that was generated but never actually charged the customer. A refund that was issued but never paid off. A push notification that never reached someone's phone. The architecture says, the message failed gracefully. The customer says, why didn't this work? Now, the worst case. One bad message, a poisoned message that nothing can parse. Look at the default SNS retry policy. Three immediate, two at one second, 10 exponential. By the time it's done, That single payload has been retried over 100,000 times across 23 days. One malformed event, droning every legitimate alert in noise. Good DLQ operations mean three things in place from day one. CloudWatch alarms on queue depth, anything above zero pages somebody. a runbook that explains what to do when the alarms fire, reply or discard, and a weekly review where someone actually opens the queue and declares it. Without those three, your DLQ is a graveyard. Failures are not visible by default. You have to make them visible. Lie number four, the dual-right problem. Most producers do two things at once. They have to save something into a database and then... They publish an event. Two systems, two retries, no transaction across them. That tiny gap is where things break. Watch what happens. The producer saves the order to its database. Database confirms it. Then it calls the broker to publish the order placed event. Between those two calls, the producer crashes. Pod gets killed. Server reboots. Network hiccups. The order exists in the database, the event never went out. Downstream services have no idea anything happened. You may think, fine, I will publish first, then save, but it's the same problem, but in a different direction. Broker accepts the event, database write fails, now your downstream consumer thinks the order exists, and your own database disagrees. Either way, your two systems are out of sync. The fix has a name. Which is transactional outbox inside one database transaction atomic all or nothing? You write the business row and a row to an outbox table both succeed or both fail Then a separate piece of code a relay dynamo DB streams Postgres CDC event bridge pipes watches the outbox table pick up the new rows Publish them the producer never talks to the broker directly If the relay crashes, it picks up where it left off. Now, event publishing is just another database, right? Line number five, sagas and schemas. The last trap is two traps in one, sagas and schemas. Both show up after the system been running long enough that everyone's forgotten what they shipped six months ago. Most business flows around one event. They are a chain. place the order, charge the card, reserve inventory, ship it. Four services, four events, running in SQL. What happens when step three fail? You cannot rule back like a database transaction. You design compensating events, refund payment, release inventory. Each step has to know how to undo itself. And while all that unwinding is happening, the user is staring at a pending state. Eventual consistency isn't a backend problem, it's a UX problem. Now, the second trap. Day 1, your event has a customer ID field. Six months later, the team owning the producer renames it to user ID. They ship two weeks pass, and all consumer crashes, and nobody remembers the rename. The change pass it code review, pass it integration test. It just didn't pass the contract that lived only inside three other services. A schema registry stops this. Every publish gets checked against the contract. Renames, removals, type change, all rejected at publish time, not at 3 a.m. The rule is simple. Add optional fields, never rename, and never remove. Version when you have to break compatibility. Treat every event like a public API. Because in a distributed system, that's exactly what it is. Treat every event like a public API. So is event-driven worth all of this? The honest answer, yes. When fanout is real. When five teams need the same event. When workflows are async by nature. EDA wins. But if only one consumer ever reads it you are not running or building an EDA You are building an indirect function call with extra latency and worse logs Before you adopt event driven six things need to be in place one idempotent consumers Every event has a stable ID to a schema registry every publish to check against the contract 3. DLQ monitoring with real alarms, not just a queue, eyes on it. Correlation IDs propagated across every service, the transactional outbox on every producer, and a product team that can show eventual consistency to users without lying about it. If you cannot operate it, you're not ready to adopt it. Operational maturity comes first, the architecture comes second. If this saved you a 3 a.m. page, hit likes and hit subscribe for more architectural deep dives. I will see you in the next video.
⚙️ Pipeline jobs
| Stage | Status | Att. | Updated | Error |
|---|---|---|---|---|
| download | done | 1/3 | 2026-07-20 15:09:08 | |
| transcribe | done | 1/3 | 2026-07-20 15:09:16 | |
| summarize | done | 1/3 | 2026-07-20 15:09:40 | |
| embed | done | 1/3 | 2026-07-20 15:09:41 |
📄 Описание YouTube
Показать
Event-driven architecture looks clean on the whiteboard. In production, it lies to you. Here are the 5 hidden traps every EDA system has — and the patterns that fix them before they wake you up at 3 AM. Most teams ship Kafka, SQS, or SNS thinking the broker handles the hard parts. It doesn't. Delivery is not correctness. Order is not guaranteed. DLQs are graveyards. Dual writes drift. Schemas break consumers six months later. This video walks through each failure mode with concrete production scenarios — double-charged customers, ghost orders, poison messages retried 100,000 times — and the exact pattern that prevents it. 🔑 What you'll learn: • Why "exactly once" delivery still double-charges users • How idempotency keys + processed-message tables fix retries • Why SQS has no ordering guarantees and Kafka only orders per-partition • The compensating events pattern for distributed sagas • Transactional outbox: solving dual writes with a single DB transaction • Schema registry rules that prevent silent consumer crashes • 6 operational prerequisites before you adopt EDA in production ⏱️ Chapters 00:00 The Whiteboard Lies 01:10 Lie #1 — Delivery ≠ Business Correctness (Idempotency) 03:02 Lie #2 — The Ordering Illusion 04:18 Lie #3 — DLQs & Poison Messages 05:59 Lie #4 — The Dual-Write Problem (Transactional Outbox) 07:41 Lie #5 — Sagas & Schemas 09:45 Is Event-Driven Even Worth It? 10:09 6 Prerequisites Before You Adopt EDA 10:50 The Real Rule: Operational Maturity First 📚 Patterns covered: idempotency keys, event versioning, dead letter queue monitoring, transactional outbox, change data capture (CDC), saga compensating transactions, schema evolution, eventual consistency. 👤 CONNECT WITH ME 🔗 LinkedIn: https://linkedin.com/in/joud-awad 🐦 X/Twitter: https://x.com/TheJoud97 📝 Medium: https://medium.com/@joudwawad 📧 Substack: https://joudawad.substack.com/ 🌐 Website: https://joudawad.com/ #EventDrivenArchitecture #SoftwareArchitecture #DistributedSystems