← все видео

The Outbox Pattern Explained — How to Never Lose a Kafka Message Again in Spring Boot

GK TechVerse · 2026-07-13 · 13м 53с · 20 просмотров · YouTube ↗

Топики: durable-execution

🎧 Аудио

📝 Summary

model=deepseek-v4-flash · prompt=summary-v7 · 4 461→2 360 tokens · 2026-07-20 15:08:38

🎯 Главная суть

Order service сначала сохраняет заказ в БД, а затем публикует событие в Kafka. Между этими двумя операциями есть промежуток — если сервис упадет после сохранения заказа, событие потеряется, и сага (распределенная транзакция) навсегда зависнет. Transactional outbox pattern решает это: событие пишется в ту же БД в рамках одной транзакции с бизнес-данными, а отдельный процесс (event relay) позже доставляет его в брокер. Это гарантирует, что событие не будет потеряно, даже после crash.


Проблема: потеря события после сохранения заказа

В микросервисной архитектуре order service выполняет две операции: сохраняет заказ в БД и публикует событие OrderCreated в брокер. Между этими шагами есть «микро-промежуток». Если после успешного commit транзакции БД сервис упадет (crash, перезагрузка, сетевой сбой), событие не будет опубликовано. База данных содержит заказ, но inventory service ничего о нем не знает, сага не продолжается, и транзакция остается «висящей». Система выглядит здоровой, но бизнес-операция навсегда заблокирована.

Почему @Transactional не спасает: dual write problem

Многие Spring-разработчики пытаются поместить обе операции (save order + publish event) в одну @Transactional аннотацию. Однако @Transactional управляет только транзакцией базы данных. Брокер событий — отдельная система с собственным сетевым соединением и условиями отказа. Возникают два сценария:

  1. БД успешно записала заказ, публикация события не удалась — заказ существует, но сага не продолжается.
  2. Если поменять порядок (сначала публиковать событие, потом сохранять заказ), событие уходит, а потом БД откатывается — inventory service получает событие о несуществующем заказе (phantom event).

Это классическая проблема dual write: невозможно атомарно обновить две независимые системы без единой распределенной транзакции. try-catch с повторными попытками решает временные сбои, но не гарантирует консистентность.

Решение: transactional outbox pattern

Вместо того чтобы публиковать событие напрямую в брокер внутри бизнес-транзакции, приложение сохраняет событие в ту же БД — в специальную таблицу outbox. И бизнес-данные, и событие находятся в одном экземпляре БД. Обе операции (save order + insert into outbox) выполняются в одной транзакции:

После crash сервиса событие не теряется: оно физически лежит в таблице outbox. Когда сервис восстанавливается, не нужно ничего «воссоздавать» — событие уже там.

Две роли: order service и event relay

Полный поток разделен на две независимые ответственности:

Это устраняет зависимость бизнес-запроса от доступности брокера в момент обработки запроса. Инвентарный сервис получает событие только после того, как relay его доставит.

Два подхода к реализации event relay

  1. Polling Publisher (опрашивающий публикатор)
    Relay периодически (например, каждые несколько секунд) опрашивает outbox-таблицу через SQL-запрос: SELECT * FROM outbox WHERE status = 'pending'. Находит новые события, публикует их в брокер, обновляет статус на published.
    Простота реализации, не требует дополнительной инфраструктуры. Компромисс: между опросами возникает задержка (латентность), что увеличивает общее время доставки. Для многих систем это приемлемо.

  2. Change Data Capture (CDC)
    Вместо опросов CDC-коннектор (например, Debezium) читает лог изменений (WAL) БД. Когда в outbox-таблицу вставляется новая строка, коннектор автоматически узнает об этом и публикует событие в брокер почти в реальном времени.
    Минимальная задержка, но требует дополнительной инфраструктуры (Debezium, Kafka Connect) и больше операционной сложности.

Выбор зависит от требований к задержке: для простых систем — Polling Publisher (легче), для высоконагруженных и чувствительных к латентности — CDC.

At-least-once delivery и идемпотентность потребителей

Event relay выполняет три шага: прочитать событие, опубликовать, отметить как опубликованное. Между шагом 2 и 3 есть крошечное окно сбоя — если relay упал после успешной публикации, но до обновления статуса, после перезапуска он прочитает то же событие снова и опубликует повторно.
Как следствие, outbox pattern обеспечивает доставку как минимум один раз (at-least-once). Каждый потребитель (inventory service и т.д.) должен быть идемпотентным: иметь уникальный event.id, проверять, обрабатывалось ли уже это событие, и пропускать дубликаты. Так бизнес-операция выполняется ровно один раз, даже если событие приходит дважды.

Структура outbox-таблицы

Таблица outbox — обычная реляционная таблица, минимум колонок:

При сохранении заказа вставляется строка с status = 'pending'. Relay периодически находит такие строки, публикует и обновляет статус.
Со временем таблица растет, поэтому опубликованные записи нужно периодически очищать (например, удалять строки с status = 'published' старше N дней).

📜 Transcript

en · 2 104 слов · 30 сегментов · clean

Показать текст транскрипта
In the previous video, we solved one of the biggest problems in microservices. A business operation spans multiple services, one step fails and instead of trying to roll back every database, the saga pattern runs compensating actions. That solves the distributed transaction problem, but there is still one dangerous gap and if we ignore it, the saga itself can silently stop. The order service creates an order, then it publishes an order created event. The inventory service receives that event and continues the saga. It's simple, right? But look carefully at what the order service actually has to do. There is two operations. First, save the order to the database, then publish the event. Two operations. And there is a tiny gap between them. That tiny gap can break the entire saga. The order is saved. The database transaction commits successfully and then before the event is published, the service crashes. Maybe the process stops. Maybe the server restarts. Maybe the network fails. The exact reason doesn't matter. What matters is this. The database now says the order exists, but the event was never published. So the inventory service knows nothing about this order. The next step never starts. No stock is reserved. The saga doesn't continue and the dangerous part is the order service may restart normally. The database is healthy, the event broker is healthy, every service looks healthy. But one business transaction is now permanently stuck. This is the real problem. The business data was saved but the event that should continue the saga was lost. So here is the question for this video. How do we guarantee that? Once the database commits succeeds, the event is never lost. We cannot simply hope the next line of code runs. We need a design that survives the crashes. And before we see the solution, we need to understand why even transactional cannot save us. Now the first solution most Spring developers will try is obvious. We already have transactional. So why not put both operations inside one transaction? Now it looks safe right? The method starts a transaction, the order is saved, the event is published and if anything fails, Spring should roll everything back. Right? Not exactly. Because transactional controls the database transaction only. It can commit the order or it can roll back the order. But the event broker is a completely separate system. It has its own network connection, its own storage, its own failure conditions. So these two operations are not part of one atomic transaction. And that creates two dangerous failure scenarios. The failure one, database succeeds, event fails. The order is saved successfully, but the event cannot published. Now the order exists, but the saga never continues. This is the failure we saw in the previous section. And the second failure is, event succeeds, database fails. Now imagine we reverse the order. Maybe we think, publish the event first. then save the order. The event is published successfully. The inventory service receives it but then the database operation fails. Now the next service starts processing an order that does not even exist. We solve the missing event problem and created a phantom event problem. This problem has a name the dual write problem. One business operation needs to update two different systems and we need Both two succeed together but there is no single local transaction covering both. Then immediately we think why can't try catch doesn't fix it. You might think if publishing fails, I will catch the exception and retry. That helps with temporary failures but it still doesn't guarantee consistency. What we really need is one atomic operation. Either the business data and the event are both saved or neither is saved. two separate systems. So how do we make that possible? The answer is surprisingly simple. Stop publishing the event to the broker inside the business transaction. Instead, store the event in the same database as the business data. That is the idea behind the transactional outbox pattern. Now we know what we need. The order and the event must be saved together. Either both succeed or both roll back. But how can we do that? when the event broker is a completely separate system? The answer is simple. Don't send the event to the broker yet. First, put it somewhere safe. Imagine you are working in an office. You write an important letter. Normally, you might wait for the delivery person and hand it over directly. But what if the delivery person is not available? Do you keep waiting? Do you carry the letter with you? No, right? You place it an outbox tray. Once the letter is inside the outbox tray, your job is complete. You don't need the delivery person to be standing next to you. Even if you leave the office, the letter is still there. Later, a separate delivery person picks it up and delivers it. That is exactly how the outbox pattern works. Instead of publishing the event directly to the event broker, the order service writes the event into the outbox table. And here is the most important part. The outbox table lives in the same database as the orders table. Now the service performs two writes, but this time they are not writes to different systems. Both writes go to the same database. First, save the order. Second, save the event in the outbox table. Because both operations are inside the same database transaction, so the database can guarantee this. If the transaction commits, the order exists. and the event is safely waiting in the outbox table. If the transaction roll back, neither exists. Now let's test the exact failure from section 1. The order is saved. The outbox event is saved. The transaction commits. And immediately after that, the service crashes. Let's say, did we lose the event? No, right? The service crashed, but the event is still safely stored in the database. When the service comes back, we don't need to recreate the event. we don't need to guess what happened because the event is still waiting in the database. This is the key idea behind the transactional outbox pattern. We did not make the database and the event broker part of one transaction. Instead, we remove the need to update both at the same time. The business transaction now talks only to the database and the event broker becomes someone else's responsibility. A separate process can pick up the event later and deliver it. So the next question is who picks up the event from the outbox table and how does it reach the event broker. That's what we will see in next. We now understand why the outbox pattern works. The order service stores the order and the event in the same database transaction. Now let's connect all these pieces and see the complete flow. The complete flow has two separate responsibilities. The first responsibility belongs to the order service. The second belongs to the event relay. Let's follow them. The client places an order. The order service starts one local database transaction. Inside this transaction, the order service performs only two database operations. It saves the order and it saves the matching event in the outbox table. Notice what is missing from the code. There is no call to the event broker. The order service talks only to its own database. Once the transaction commits, its job is complete. Now the event is waiting in the outbox table. A separate component called the event relay finds this event. Its job is simple. The relay publishes order created to the event broker. The broker delivers it to the inventory service. And the next step of the saga begins. This separation is the heart of the complete architecture. The order service is responsible for safely storing the event. The event relay is responsible for delivering the event. The business request no longer depends on the event broker being available at that exact moment. But now we have one important question. The event relay needs to find new events inside the outbox table. How does it know when a new event is waiting? There are two common approaches, Polling Publisher and Change Data Capture. Let's compare them next. There are two common approaches, Polling Publisher and Change Data Capture. Approach 1, Polling Publisher. Polling is the simpler approach. The relay checks the outbox table at regular intervals. Every few seconds, the relay asks the database, are there any pending events? It might run a query like this. If it finds new events, It publishes them to the broker. It's simple right? And for many applications, this approach works perfectly well. You don't need extra infrastructure. A schedule background process can handle it. But polling has one trade-off. Imagine the relay checks the table now one millisecond later. A new event is created. That event has to wait until the next poll. So polling always needs a balance between latency and database load. For many systems that is completely acceptable but there is another approach instead of repeatedly asking the database did anything change? What if we could detect the change automatically? That is approach to change data capture which is CDC. CDC does not repeatedly query the outbox table. Instead it watches the database change log. Production database already maintain a log of changes. When the order service inserts a new row into the outbox table, that change appears in the database log. A CDC connector watches this log. When it detects a new outbox event, it publishes that event to the broker. So the difference is simple. With polling, the relay keeps asking the database for new events. With CDC, a relay reacts when the database changes. Tools like DebiGM can read database change logs. and capture new outbox records. So for simple system, polling is usually easier. It is easy to build, easy to understand and easy to operate. For high volume, systems where lower event latency matters, CDC can be a better fit. But it also adds more infrastructure and operational complexity. We just found another small failure window. The relay performs three steps. Read event, publish event, Mark has published. Now imagine this happens. The event is published successfully but before the relay updates the outbox table it crashes. The event broker has already received event. The next service may have already processed it. But the outbox table still says event is pending. When the relay restarts it finds the same pending event and publishes it again. Now the inventory service receives the same event twice. Because after publishing the event, it must record that the publish succeed and there is always a tiny gap between those two operations. So the safe choice is to publish again. This means the outbox pattern usually gives us at least once delivery. And that means every consumer must be ready for duplicates. This is where idempotency becomes important. Every event should have a unique event id. The first time event arrives, the consumer checks its processed event records. The event ID is not there, so it process the event, then it stores that event as processed. Now imagine the same event arrives again. The consumer checks the event ID. This time that event already exists, so the consumer skips it. So the same event may arrive two times or even more, but the business operation still happens only once. That is an idempotent consumer. Now the reliability model is complete. The outbox pattern prevents lost events. The event relay keeps retrying until the event is delivered. And the idempotent consumer prevents duplicate processing. We have been talking about the outbox table throughout this video. But what does it actually look like? In reality, it's just another database table. Let's understand each column. Event ID uniquely identifies every event. Event type tells us what happened. Payload contains the information needed by the consumer. Status tells us whether the relay still needs to publish the event. Created it tells us when the event was created. That's all you need for a basic outbox implementation. When the order service creates an order, it also inserts one row into the outbox table. Initially, the status is pending. The event relay finds this row. publishes the event and then update the status to published. This simple status tells the relay which events still need to be delivered. One last thing as your application runs this table keeps growing so published events should be cleaned up periodically otherwise the outbox table becomes larger and larger over time.

⚙️ Pipeline jobs

StageStatusAtt.UpdatedError
download done 1/3 2026-07-20 15:08:04
transcribe done 1/3 2026-07-20 15:08:13
summarize done 1/3 2026-07-20 15:08:38
embed done 1/3 2026-07-20 15:08:39

📄 Описание YouTube

Показать
Have you ever wondered what happens if your service successfully saves data to the database but crashes before publishing an event?

Your database says the order exists.

But the next microservice never receives the event.

The Saga stops.

This is one of the most common reliability problems in event-driven microservices, known as the  Dual-Write Problem .

In this video, we explore the  Transactional Outbox Pattern , one of the most widely used patterns for building reliable event-driven systems.

You'll learn:

1.Why the Dual-Write Problem happens
2.Why `@Transactional` cannot make a database and an event broker one atomic transaction
3.How the Transactional Outbox Pattern guarantees reliable event publishing
4.How the Event Relay works
5.Polling Publisher vs Change Data Capture (CDC)
6.Why duplicate events still happen
7.What At-Least-Once Delivery means
8.How Idempotent Consumers safely handle duplicate events
9.What a real Outbox table looks like

Whether you're building Spring Boot microservices with Kafka, RabbitMQ, ActiveMQ, or any event-driven architecture, understanding the Outbox Pattern is essential for building reliable distributed systems.

Chapters

00:00 The Saga Is Still Broken

02:14 Why @Transactional Cannot Save You

03:45 The Dual-Write Problem

04:37 The Outbox Mental Model

07:15 Complete Outbox Flow

09:01 Polling Publisher vs CDC

11:04 The Duplicate Event Trap & Idempotent Consumers

12:47 What Does an Outbox Table Look Like?

If you enjoy deep dives into Java, Spring Boot, Microservices, and System Design, subscribe to GK TechVerse for production-focused content explained with simple visuals and real-world examples.

#SpringBoot #Java #Microservices #TransactionalOutbox #OutboxPattern #SagaPattern