FinCodeCow · Глава IV

Data Liberation

Как интегрировать legacy-системы в событийную архитектуру: CDC, Outbox, событизация и путь миграции

В идеальном мире мы бы начинали каждый IT-проект с чистого листа, проектируя идеальную событийно-управляемую архитектуру с нуля. Но в реальности большинство компаний — это «коричневые поля» (brownfield). У вас уже есть работающие монолиты, реляционные базы данных, унаследованные API и запутанные связи «точка-точка». Переход на микросервисы требует не просто написания нового кода, а интеграции старого с новым.

Ключевым понятием здесь становится освобождение данных (Data Liberation). Это процесс извлечения критически важных бизнес-данных из изолированных хранилищ legacy-систем и публикация их в виде потоков событий. Именно этот шаг превращает базу данных из «личного сейфа» одного монолита в «единый источник истины» для всей организации.

Это четвёртая глава цикла о событийно-управляемой архитектуре. Первая отвечала на вопрос «почему», вторая — «как устроено под капотом», третья — «как проектировать контракты». Теперь мы разберём, как подключить к этому миру legacy-системы, которые никто не собирается переписывать.

• • •

IПроблема Dual Write: почему простая публикация событий не работает

Прежде чем разбирать паттерны, нужно понять главную проблему, которую они решают. Когда сервис пытается одновременно записать данные в свою базу и опубликовать событие в брокер (Kafka, RabbitMQ), возникает проблема двойной записи (Dual Write). Эти две операции не могут быть выполнены в одной атомарной транзакции — БД и брокер — разные системы с независимыми транзакциями.

Что может пойти wrong:

  1. БД записана, брокер недоступен — данные есть, события нет. Потребители никогда не узнают об изменении.
  2. Брокер записан, БД упала — событие опубликовано, но состояние не сохранено. Потребители получат ложный факт.
  3. Частичный сбой при ретрае — продюсер ретраит публикацию, и брокер получает дубликат.

Ни один из этих сценариев недопустим для финансового учёта, инвентаризации или логистики. Именно поэтому нужны паттерны, гарантирующие атомарность.

Фундаментальный принцип

Любая система, которая пытается одновременно обновить БД и опубликовать событие, обречена на рассинхронизацию. Единственный способ её избежать — сделать так, чтобы событие было частью той же транзакции, что и изменение состояния.

• • •

IIТри паттерна извлечения данных

Чтобы разорвать спагетти синхронных вызовов и прямых запросов к чужим базам данных, нужно научиться генерировать события на основе изменений в legacy-системах. Существует три основных архитектурных паттерна.

1. Опрос на основе запросов (Query-based Polling)

Самый простой, но и самый «грязный» метод. Внешний процесс периодически опрашивает базу данных, выполняя SQL-запросы с фильтрацией по временной метке (updated_at) или автоинкрементному ID.

Плюсы

Работает с любой СУБД без специальной конфигурации. Позволяет сразу денормализовать данные и скрыть структуру таблиц через Views. Не требует прав администратора.

Минусы

Пропускает hard deletes. Создаёт паразитную нагрузку. Задержка зависит от частоты опроса. Гарантии доставки — at-most-once или at-least-once с двойной обработкой.

Когда выбирать: низкая нагрузка, простая схема, нет жёстких требований к latency, нет hard deletes, нет возможности включить CDC.

2. Перехват журналов транзакций (CDC)

Change Data Capture (CDC) — это «прослушка» внутренних журналов базы данных. Специальные коннекторы (Debezium, Maxwell, AWS DMS) читают binlog в MySQL, WAL в PostgreSQL или redo log в Oracle и превращают каждое изменение строки в событие.

Плюсы

Низкая задержка (миллисекунды). Фиксирует всё, включая hard deletes. Не нагружает БД запросами. Сохраняет порядок транзакций.

Минусы

Требует конфигурации БД. Риск переполнения WAL/slot. Раскрывает внутреннюю нормализованную модель. at-least-once — дубликаты. Чувствительность к DDL-изменениям.

Когда выбирать: высокая нагрузка, нужна real-time доставка, есть доступ к конфигурации БД, структура таблиц относительно стабильна.

3. Паттерн «Исходящая таблица» (Transactional Outbox)

Золотой стандарт для микросервисов. При изменении бизнес-сущности приложение в одной атомарной транзакции обновляет основную таблицу и записывает факт этого изменения в специальную «исходящую таблицу» (Outbox). Отдельный процесс (Relay) забирает данные из Outbox и публикует их в брокер событий.

Плюсы

Гарантирует согласованность — Dual Write решена. Позволяет публиковать богатые денормализованные бизнес-события. Полностью изолирует внутреннюю модель БД от внешнего мира.

Минусы

Требует изменения кода приложения. Для legacy с закрытым кодом неприменим. Добавляет накладные расходы на транзакции БД. Требует выбора механизма Relay.

Когда выбирать: новый код, рефакторинг монолита, критичная согласованность данных, нужна денормализация событий.

• • •

IIIOutbox Relay: три способа вытащить события из таблицы

Сам outbox — это таблица. Но как события из неё попадают в Kafka? Существуют три подхода, и выбор между ними — ключевой архитектурный момент.

Механизм Relay Как работает Плюсы Минусы
Polling-based Процесс раз в N секунд делает SELECT * FROM outbox WHERE published = false Простота, не требует CDC Нагрузка на БД, задержка, трудно масштабировать
CDC on Outbox Debezium читает outbox-таблицу как обычную CDC-таблицу Real-time, низкая нагрузка, at-least-once Нужна конфигурация CDC, дубликаты при рестарте
Application-level relay Приложение само публикует после коммита транзакции Exactly-once при правильной настройке Высокая сложность, привязка к Kafka transactions

Polling-based relay — самый распространённый стартовый вариант. Но при росте нагрузки опрос становится узким местом. CDC on Outbox — элегантное решение: Debezium читает только новые строки (через binlog/WAL), а не сканирует таблицу целиком. При этом outbox-события уже денормализованы приложением — лучшее из обоих миров.

Очистка Outbox. Записи не должны копиться вечно. Стратегии: DELETE FROM outbox WHERE published = true AND created_at < NOW() - INTERVAL '7 days' или отдельная таблица-архив. Важно: при использовании CDC on Outbox удаление уже прочитанных записей безопасно — Debezium уже получил событие из WAL/binlog.

• • •

IVЛовушка «тупых» коннекторов и магия событизации

Одна из самых частых ошибок при внедрении EDM — использование централизованных CDC-фреймворков для того, чтобы просто «зеркалировать» реляционные таблицы в топики Kafka. Команда думает: «Мы освободили данные!», но на самом деле они просто перенесли свою реляционную схему в брокер событий.

Это нарушает главный принцип микросервисов — изоляцию ограниченных контекстов. Если потребителям приходится самостоятельно делать JOIN-ы над потоками событий, чтобы восстановить смысл бизнес-сущности (например, собирать «Заказ» из потоков OrderHeader, OrderLine и Customer), вы создали распределённый монолит.

Ключевое различие

Change Data Capture — захват технических изменений (UPDATE orders SET status='shipped').
Change Data Value — бизнес-смысл изменений (OrderShipped { order_id, tracking_number, shipped_at }).

Решение: Событизация (Eventification). Вместо публикации сырых нормализованных строк, создаётся процесс, который денормализует реляционные данные в целостные бизнес-события. Потребитель получает готовый факт: «Заказ №123 создан, включает товары А и Б, сумма такая-то», а не набор разрозненных SQL-изменений.

Практическая реализация событизации — Kafka Streams или Flink pipeline, который читает сырые CDC-потоки, обогащает их (join с справочными таблицами или потоками) и публикует денормализованные события в новые канонические потоки:

Raw CDC streams (Debezium)
    ├── orders (INSERT/UPDATE/DELETE)
    ├── order_items
    └── customers
         │
         ▼
   Kafka Streams / Flink
   (join + aggregate + map)
         │
         ▼
   Canonical event streams
    ├── order.placed
    ├── order.paid
    ├── order.shipped
    └── order.cancelled
• • •

VSnapshot, Streaming и Backfill: как наполнить новые потоки историей

CDC решает задачу real-time доставки изменений, но не отвечает на вопрос: что делать с историческими данными, которые уже есть в legacy-БД? Новый потребитель, подписавшийся на канонический поток сегодня, не увидит события за прошлый год, если их никто не «переиграл».

Snapshotting: что делает Debezium при первом запуске

При первом подключении к таблице Debezium выполняет initial snapshot — читает всю таблицу и публикует по одному событию на каждую строку. Это позволяет новым потребителям получить полное состояние на момент старта.

Плюсы snapshot

Полное состояние на момент старта. Не нужно писать отдельный backfill-скрипт. Debezium сам переключается на streaming после завершения.

Минусы snapshot

Нагрузка на legacy-БД (полное чтение таблицы). Может занять часы для больших таблиц. Блокирует replication slot на время snapshot.

Backfill: переигрывание исторических данных

Если в legacy-БД есть audit_log или event_store с историческими изменениями, их можно использовать как источник для backfill канонических потоков. Это отдельная задача, отличная от CDC:

  1. Экспорт — выгрузить исторические события из legacy в отдельный файл или топик.
  2. Трансформация — применить ту же логику событизации, что и для real-time потока.
  3. Загрузка — опубликовать в канонический поток с сохранением исходного occurred_at.
  4. Reconciliation — сверить состояние материализованных проекций с состоянием legacy-БД.
Стратегия для больших таблиц

Для таблиц с миллионами строк используйте incremental snapshot (Debezium Signal Table): можно запускать snapshot по частям, не блокируя БД. Сигнал передаётся через специальную таблицу, а Debezium выполняет snapshot в фоне, продолжая streaming.

• • •

VISchema evolution при CDC: когда колонка меняется без предупреждения

Debezium автоматически генерирует схемы событий из структуры таблиц. Если в legacy-БД кто-то добавляет, переименовывает или удаляет колонку — меняется схема события, и потребители могут сломаться.

Это особенно болезненно, потому что DDL-изменения в legacy часто происходят вне цикла релиза микросервисов — DBA может выполнить миграцию в maintenance window, не уведомив команды потребителей.

Решение — связка Debezium + Schema Registry:

  1. Debezium публикует схему каждого события. При интеграции с Confluent Schema Registry схема регистрируется автоматически.
  2. Registry проверяет совместимость новой схемы с предыдущими версиями.
  3. Если DDL-изменение нарушает совместимость (например, удаление обязательной колонки) — публикация блокируется, и коннектор останавливается. Это «канарейка»: лучше остановить коннектор, чем сломать десяток потребителей.

Однако это создаёт дилемму: коннектор остановился → backlog растёт → WAL-слот в PostgreSQL переполняется. Поэтому DDL-изменения в legacy-БД, подключённой к CDC, должны быть согласованы — как релиз микросервиса, с уведомлением и планированием.

• • •

VIIОбратная сторона медали: запись в Sink-и

Освобождение данных — это только полдела. Часто возникает ситуация, когда legacy-системе (или аналитическому озеру данных) нужно получить данные из нового событийного мира. Здесь на сцену выходит паттерн Sink (Приемник).

Специальный коннектор (Kafka Connect JDBC Sink, Elasticsearch Sink, S3 Sink) подписывается на поток событий, материализует его и аккуратно вставляет/обновляет данные в целевое хранилище.

Но sink-запись в реляционную БД имеет свои подводные камни:

  1. Idempotent writes. Sink-коннектор может доставить событие дважды. Запись в БД должна быть идемпотентной — INSERT ... ON CONFLICT DO UPDATE (upsert) вместо голого INSERT.
  2. Обработка tombstones. Kafka compaction создаёт tombstone-сообщения (null value) для удалённых ключей. Sink-коннектор должен интерпретировать tombstone как DELETE FROM table WHERE id = ?. Без этого удалённые записи «воскрешаются» при перезапуске коннектера.
  3. Ordering. Если OrderUpdated придёт раньше OrderCreated (из-за рестарта или rebalance), upsert перезатрёт корректное состояние некорректным. Решение — партиционирование sink-потока по тому же ключу, что и источник, и один consumer на партицию.
  4. Batching. Sink-коннекторы обычно пишут батчами для производительности. Но батчинг может нарушить порядок отдельных операций. Для критичных сценариев — consumer.max.poll.records=1 и синхронная запись, ценой throughput.

Альтернативы Kafka Connect JDBC Sink

Инструмент Когда выбирать
Confluent JDBC Sink Managed, простая настройка, стандартные сценарии
Debezium JDBC Sink Обратная запись из Kafka в БД с сохранением семантики Debezium-событий
Materialize / Rockset Streaming materialization — непрерывные SQL-запросы над потоками
Custom consumer Специфичная логика трансформации, недоступная в стандартных коннекторах
• • •

VIIIИнструментарий и стоимость

Выбор инструмента CDC — это не только вопрос функциональности, но и стоимости владения.

Инструмент Тип БД Особенность
Debezium Open-source CDC PostgreSQL, MySQL, Oracle, SQL Server, MongoDB, Cassandra Стандарт де-факто. Работает поверх Kafka Connect.
Debezium Server Легковесный CDC Те же + Redis Stream, Pulsar, Kinesis Для случаев, когда Kafka Connect — оверхед.
Maxwell CDC для MySQL MySQL только Простой, но ограниченный — только MySQL.
AWS DMS Managed CDC Множество БД Managed-сервис AWS, менее гибкий, чем Debezium.
Confluent Connectors Managed connectors Множество Managed Kafka Connect + Debezium.
Custom polling Query-based Любая Когда CDC недоступен, а Outbox нельзя внедрить.

Стоимость: self-hosted vs managed

Компонент Self-hosted Managed
Kafka 3+ брокера, Zookeeper/KRaft, мониторинг Confluent Cloud, MSK, Redpanda Cloud
Kafka Connect Отдельный кластер, масштабирование, обновления MSK Connect, Confluent Connectors
Schema Registry Apicurio, Karapace Confluent Schema Registry, AWS Glue
Replication slots Дополнительный I/O и место на диске legacy-БД —
Хранение событий Диски + retention policy Оплата за объём и throughput

Правило: self-hosted выгоден при стабильной нагрузке и наличии SRE-команды. Managed — при переменной нагрузке и желании сократить операционные расходы. При росте throughput managed может стать дороже, чем self-hosted — считайте стоимость на 1M событий.

• • •

IXObservability CDC-пайплайна: что мониторить

CDC-пайплайн — это не «настроил и забыл». Без мониторинга вы узнаете о проблеме только когда WAL-слот переполнится, и legacy-БД упадёт.

Kafka Connect metrics

  • source-record-poll-rate — сколько записей читает коннектор из БД;
  • source-record-write-rate — сколько публикует в Kafka;
  • poll-batch-avg-time-ms — время на чтение батча;
  • offset-commit-success-percentage — успешность коммитов offset.

Debezium metrics

  • MilliSecondsBehindSource — отставание от источника (в миллисекундах);
  • NumberOfEventsFiltered — сколько событий отфильтровано;
  • NumberOfCommittedTransactions — число закоммиченных транзакций.

PostgreSQL replication slot lag

SELECT slot_name,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag
FROM pg_replication_slots
WHERE slot_name = 'debezium_orders';

Если lag растёт — коннектор отстаёт. Если lag превышает размер диска — БД упадёт. Алерт должен срабатывать при lag > 1 ГБ или при росте за 15 минут.

Правило трёх алертов

Минимум три алерта на CDC-пайплайн: (1) отставание коннектора > порога, (2) ошибки в Kafka Connect, (3) рост replication slot lag. Без любого из них вы работаете вслепую.

• • •

XТестирование CDC-пайплайна

CDC сложнее тестировать, чем обычный код: нужна реальная БД, реальный брокер, реальный коннектор. Решение — Testcontainers.

  1. Testcontainers с PostgreSQL — поднять контейнер с wal_level=logical, подключить Debezium, проверить pipeline end-to-end.
  2. Проверка дубликатов при рестарте — остановить коннектор, изменить данные, запустить снова. Убедиться, что потребитель корректно обрабатывает дубликаты.
  3. Проверка hard deletes — удалить строку, убедиться, что Debezium публикует событие с op: "d".
  4. Проверка schema evolution — добавить колонку в БД, убедиться, что Schema Registry принял новую схему или отклонил несовместимую.
  5. Проверка tombstone handling — убедиться, что sink корректно удаляет запись при получении tombstone.
• • •

XIПрактический сценарий: путь миграции за 6 месяцев

Статья описывает паттерны, но как выглядит реальный путь? Вот типичный сценарий для монолита на PostgreSQL, который нужно интегрировать в событийную архитектуру.

Месяц 1–2

Инвентаризация и подготовка

Определить, какие таблицы legacy-БД содержат данные, нужные другим сервисам. Настроить wal_level=logical, создать replication slot, проверить мониторинг lag. Установить Kafka + Schema Registry + Kafka Connect. Настроить Debezium на 1–2 пилотных таблицах.

Месяц 3–4

Событизация

Разворачивать Kafka Streams / Flink pipeline для денормализации сырых CDC-потоков в канонические бизнес-события. Зарегистрировать схемы событий в Schema Registry с режимом BACKWARD_TRANSITIVE. Подключить первых потребителей — пусть читают канонические потоки вместо прямых запросов к БД монолита.

Месяц 5

Strangler fig для данных

Новые сервисы создаются как потребители канонических потоков, а не как клиенты legacy-БД. Прямые запросы к БД монолита постепенно переводятся на чтение из потоков (через материализованные проекции). Монолит получает Outbox-паттерн для новых фич.

Месяц 6

Передача ownership

Команда монолита официально становится «производителем событий» — отвечает за схему, качество и SLA публикации. Прямые запросы к БД монолита извне запрещены на уровне архитектурного комитета. Монолит начинает потреблять собственные события (event sourcing для новых фич).

Это не линейный процесс — каждая фаза может итерироваться. Но направление движения именно такое: от сырых CDC-потоков к каноническим событиям, от прямых запросов к подпискам, от «закрытой БД» к «открытому потоку фактов».

• • •

XIIКультурный сдвиг: от Pull к Event-First

Если вы просто настроите CDC-коннекторы, чтобы «перекачать» данные из монолита в Kafka, и на этом остановитесь, вы не построили событийную архитектуру. Вы просто построили более сложный ETL-процесс. Монолит останется монолитом, а команды не научатся мыслить категориями событий.

Истинная цель Data Liberation — не просто переместить байты, а изменить культуру владения данными. Команда, владеющая монолитом, должна осознать себя как производителя событий. Они несут ответственность за качество, схему и семантику того, что они публикуют.

Со временем, по мере рефакторинга, монолит должен начать сначала генерировать события, и только затем (если ему самому это нужно) потреблять их обратно для материализации своего локального состояния. Брокер событий становится не просто «трубой» для legacy-данных, а настоящим центром нервной системы предприятия.

• • •

XIIIСвязь с циклом и синтез

Эта глава — четвёртая в цикле о событийно-управляемой архитектуре. Она опирается на три предыдущие.

Глава Фокус Связь с текущей главой
I. Среда формирует архитектуру Почему EDA и как это меняет организации Культурный сдвиг при Data Liberation — прямое следствие закона Конвея
II. Анатомия EDA Как устроено под капотом Consumer groups, offsets, идемпотентность — всё это применимо к CDC-потребителям
III. Анатомия идеального события Как проектировать контракты Schema Registry и эволюция схем — критичны для CDC из legacy
IV. Data Liberation Как подключить legacy к EDA Событизация как мост между реляционной моделью и бизнес-событиями

Ключевые выводы главы

  1. Dual Write — фундаментальная проблема, которую решают все три паттерна разными способами. Игнорировать её — значит строить на ненадёжном фундаменте.
  2. CDC (Debezium) — технически лучший способ извлечения, но он даёт сырые CRUD-события, а не бизнес-факты. Событизация — обязательный следующий шаг.
  3. Transactional Outbox — единственный паттерн, который позволяет публиковать денормализованные бизнес-события с гарантией атомарности, но требует изменения кода.
  4. Snapshot + Backfill — обязательные шаги для наполнения новых потоков историей. Без них новые потребители не увидят прошлое.
  5. Schema Registry + CDC — критическая связка: без неё DDL-изменения в legacy-БД молча ломают потребителей.
  6. Sink-запись — не тривиальна: idempotent writes, tombstone handling и ordering guarantees должны быть продуманы.
  7. Observability — минимум три алерта: отставание коннектора, ошибки Kafka Connect, рост replication slot lag.
  8. Стратегия миграции — постепенная (strangler fig), от пилотных таблиц к каноническим потокам, с передачей ownership команде-производителю.

Чеклист перед запуском CDC в production

  • Настроен wal_level=logical (PostgreSQL) или binlog_format=ROW (MySQL)
  • Создан отдельный replication slot под каждый коннектор
  • Настроен мониторинг replication slot lag с алертом
  • Schema Registry подключён, режим совместимости — BACKWARD_TRANSITIVE
  • Настроена событизация: CDC-потоки трансформируются в канонические бизнес-события
  • Pipeline протестирован через Testcontainers, включая hard deletes и schema evolution
  • Sink-запись идемпотентна, обрабатывает tombstones и сохраняет порядок
  • DDL-изменения в legacy-БД согласовываются с командой потребителей
  • Проведён backfill исторических данных для новых потребителей
  • Команда монолита осознаёт себя производителем событий и отвечает за их качество
Финальный тезис

Интеграция legacy-систем — это самый сложный, но неизбежный этап зрелости любой EDM-архитектуры. Технология — лишь средство. Главный критерий успеха — способность организации перейти от модели «закрытых баз данных» к модели «открытых потоков фактов», где каждая система уважает границы других и общается через чётко определённые, денормализованные контракты событий.

Глава IV цикла «Событийно-управляемая архитектура». Источники: Debezium, Confluent, Microservices.io, Baeldung, Habr, Conduktor, Microsoft Azure Architecture Center, практические кейсы production-систем.

FinCodeCow · Chapter IV

Data Liberation

Integrating legacy systems into event-driven architecture: CDC, Outbox, eventification, and the migration path

In an ideal world, we would start every IT project from scratch, designing the perfect event-driven architecture. But in reality, most companies are brownfield. You already have working monoliths, relational databases, legacy APIs, and tangled point-to-point connections. Moving to microservices requires not just writing new code but integrating the old with the new.

The key concept here is Data Liberation. It is the process of extracting critical business data from isolated legacy stores and publishing it as event streams. This step turns a database from a "personal safe" of one monolith into a "single source of truth" for the entire organization.

This is the fourth chapter in the event-driven architecture series. The first answered "why," the second — "how it works under the hood," the third — "how to design contracts." Now we will explore how to connect legacy systems that no one is going to rewrite to this world.

• • •

IThe Dual Write Problem: Why Simple Event Publishing Doesn't Work

Before diving into patterns, we need to understand the main problem they solve. When a service tries to simultaneously write to its database and publish an event to a broker (Kafka, RabbitMQ), the Dual Write problem arises. These two operations cannot be performed in a single atomic transaction — the DB and the broker are different systems with independent transactions.

What can go wrong:

  1. DB written, broker unavailable — data exists, no event. Consumers will never learn about the change.
  2. Broker written, DB crashed — the event is published, but the state is not saved. Consumers receive a false fact.
  3. Partial failure on retry — the producer retries the publication, and the broker receives a duplicate.

None of these scenarios is acceptable for financial accounting, inventory, or logistics. This is why patterns that guarantee atomicity are needed.

Fundamental Principle

Any system that tries to update the DB and publish an event simultaneously is doomed to desynchronization. The only way to avoid it is to make the event part of the same transaction as the state change.

• • •

IIThree Data Extraction Patterns

To break the spaghetti of synchronous calls and direct queries to foreign databases, we need to learn to generate events based on changes in legacy systems. There are three main architectural patterns.

1. Query-based Polling

The simplest but "dirtiest" method. An external process periodically polls the database, executing SQL queries filtered by timestamp (updated_at) or auto-increment ID.

Pros

Works with any DBMS without special configuration. Allows immediate denormalization and hiding table structure via Views. No admin rights required.

Cons

Misses hard deletes. Creates parasitic load. Latency depends on polling frequency. Delivery guarantees — at-most-once or at-least-once with double processing.

When to choose: low load, simple schema, no strict latency requirements, no hard deletes, no ability to enable CDC.

2. Change Data Capture (CDC)

CDC is "listening" to internal database logs. Special connectors (Debezium, Maxwell, AWS DMS) read binlog in MySQL, WAL in PostgreSQL, or redo log in Oracle and turn every row change into an event.

Pros

Low latency (milliseconds). Captures everything, including hard deletes. Doesn't load the DB with queries. Preserves transaction order.

Cons

Requires DB configuration. Risk of WAL/slot overflow. Exposes the internal normalized model. at-least-once — duplicates. Sensitive to DDL changes.

When to choose: high load, real-time delivery needed, DB configuration access available, table structure relatively stable.

3. Transactional Outbox

The gold standard for microservices. When a business entity changes, the application updates the main table and writes the fact of this change to a special "outbox table" in one atomic transaction. A separate process (Relay) takes data from the Outbox and publishes it to the event broker.

Pros

Guarantees consistency — Dual Write is solved. Allows publishing rich, denormalized business events. Fully isolates the internal DB model from the outside world.

Cons

Requires application code changes. Not applicable to closed-source legacy. Adds overhead to DB transactions. Requires choosing a Relay mechanism.

When to choose: new code, monolith refactoring, critical data consistency, event denormalization needed.

• • •

IIIOutbox Relay: Three Ways to Extract Events

The outbox itself is a table. But how do events from it get into Kafka? There are three approaches, and the choice between them is a key architectural decision.

Relay Mechanism How It Works Pros Cons
Polling-based Process every N seconds runs SELECT * FROM outbox WHERE published = false Simplicity, no CDC needed DB load, latency, hard to scale
CDC on Outbox Debezium reads the outbox table as a regular CDC table Real-time, low load, at-least-once CDC configuration needed, duplicates on restart
Application-level relay Application publishes after transaction commit Exactly-once with proper setup High complexity, Kafka transactions dependency

Polling-based relay is the most common starting point. But as load grows, polling becomes a bottleneck. CDC on Outbox is an elegant solution: Debezium reads only new rows (via binlog/WAL), not the whole table. And outbox events are already denormalized by the application — best of both worlds.

Outbox cleanup. Records should not accumulate forever. Strategies: DELETE FROM outbox WHERE published = true AND created_at < NOW() - INTERVAL '7 days' or a separate archive table.

• • •

IVThe Trap of "Dumb" Connectors and the Magic of Eventification

One of the most common mistakes in adopting EDM is using centralized CDC frameworks to simply "mirror" relational tables into Kafka topics. The team thinks: "We liberated the data!" but in fact they just moved their relational schema into the event broker.

This violates the main principle of microservices — bounded context isolation. If consumers must do JOINs over event streams to reconstruct the meaning of a business entity, you have created a distributed monolith.

Key Distinction

Change Data Capture — capturing technical changes (UPDATE orders SET status='shipped').
Change Data Value — the business meaning of changes (OrderShipped { order_id, tracking_number, shipped_at }).

Solution: Eventification. Instead of publishing raw normalized rows, a process denormalizes relational data into holistic business events. The consumer receives a ready-made fact: "Order #123 created, includes items A and B, total is X," not a set of scattered SQL changes.

Practical implementation — a Kafka Streams or Flink pipeline that reads raw CDC streams, enriches them (joins with reference tables or streams), and publishes denormalized events to new canonical streams:

Raw CDC streams (Debezium)
    ├── orders (INSERT/UPDATE/DELETE)
    ├── order_items
    └── customers
         │
         ▼
   Kafka Streams / Flink
   (join + aggregate + map)
         │
         ▼
   Canonical event streams
    ├── order.placed
    ├── order.paid
    ├── order.shipped
    └── order.cancelled
• • •

VSnapshot, Streaming, and Backfill: Filling New Streams with History

CDC solves the real-time delivery task but does not answer the question: what to do with historical data already in the legacy DB? A new consumer subscribing to a canonical stream today will not see last year's events unless someone "replays" them.

Snapshotting: What Debezium Does on First Start

On the first connection to a table, Debezium performs an initial snapshot — reads the whole table and publishes one event per row. This allows new consumers to get the full state at the moment of start.

Snapshot Pros

Full state at start moment. No need for a separate backfill script. Debezium switches to streaming after completion.

Snapshot Cons

Load on the legacy DB (full table read). Can take hours for large tables. Blocks the replication slot during snapshot.

Backfill: Replaying Historical Data

If the legacy DB has an audit_log or event_store with historical changes, it can be used as a source for backfilling canonical streams:

  1. Export — extract historical events from legacy into a separate file or topic.
  2. Transformation — apply the same eventification logic as for the real-time stream.
  3. Load — publish to the canonical stream preserving the original occurred_at.
  4. Reconciliation — verify that materialized projections match the legacy DB state.
Strategy for Large Tables

For tables with millions of rows, use incremental snapshot (Debezium Signal Table): you can run snapshot in parts without blocking the DB. The signal is passed through a special table, and Debezium performs the snapshot in the background while continuing streaming.

• • •

VISchema Evolution in CDC: When a Column Changes Without Warning

Debezium automatically generates event schemas from table structures. If someone in the legacy DB adds, renames, or deletes a column — the event schema changes, and consumers may break.

This is especially painful because DDL changes in legacy often happen outside the microservice release cycle — a DBA may run a migration in a maintenance window without notifying consumer teams.

Solution — Debezium + Schema Registry:

  1. Debezium publishes the schema of each event. With Confluent Schema Registry integration, the schema is registered automatically.
  2. The registry checks compatibility of the new schema with previous versions.
  3. If a DDL change violates compatibility (e.g., removing a required column), publication is blocked, and the connector stops. This is a "canary": better to stop the connector than break a dozen consumers.

However, this creates a dilemma: the connector stops → backlog grows → WAL slot in PostgreSQL overflows. Therefore, DDL changes in a CDC-connected legacy DB must be coordinated — like a microservice release, with notification and planning.

• • •

VIIThe Other Side of the Coin: Sink Writes

Data liberation is only half the battle. Often a legacy system (or analytical data lake) needs to receive data from the new event world. This is where the Sink pattern comes in.

A special connector (Kafka Connect JDBC Sink, Elasticsearch Sink, S3 Sink) subscribes to an event stream, materializes it, and carefully inserts/updates data in the target store.

But sink writes to a relational DB have their own pitfalls:

  1. Idempotent writes. The sink connector may deliver an event twice. DB writes must be idempotent — INSERT ... ON CONFLICT DO UPDATE (upsert) instead of a raw INSERT.
  2. Tombstone handling. Kafka compaction creates tombstone messages (null value) for deleted keys. The sink connector must interpret a tombstone as DELETE FROM table WHERE id = ?. Otherwise, deleted records "resurrect" on connector restart.
  3. Ordering. If OrderUpdated arrives before OrderCreated (due to restart or rebalance), the upsert overwrites correct state with incorrect. Solution — partition the sink stream by the same key as the source, one consumer per partition.
  4. Batching. Sink connectors usually write in batches for performance. But batching can violate individual operation order. For critical scenarios — consumer.max.poll.records=1 and synchronous writes, at the cost of throughput.

Alternatives to Kafka Connect JDBC Sink

Tool When to Choose
Confluent JDBC Sink Managed, simple setup, standard scenarios
Debezium JDBC Sink Reverse write from Kafka to DB preserving Debezium event semantics
Materialize / Rockset Streaming materialization — continuous SQL queries over streams
Custom consumer Specific transformation logic not available in standard connectors
• • •

VIIITools and Cost

Choosing a CDC tool is not just about functionality but also cost of ownership.

Tool Type DB Feature
Debezium Open-source CDC PostgreSQL, MySQL, Oracle, SQL Server, MongoDB, Cassandra De facto standard. Runs on Kafka Connect.
Debezium Server Lightweight CDC Same + Redis Stream, Pulsar, Kinesis For cases where Kafka Connect is overhead.
Maxwell CDC for MySQL MySQL only Simple but limited — MySQL only.
AWS DMS Managed CDC Multiple DBs AWS managed, less flexible than Debezium.
Confluent Connectors Managed connectors Multiple Managed Kafka Connect + Debezium.
Custom polling Query-based Any When CDC is unavailable and Outbox cannot be implemented.

Cost: Self-hosted vs Managed

Component Self-hosted Managed
Kafka 3+ brokers, Zookeeper/KRaft, monitoring Confluent Cloud, MSK, Redpanda Cloud
Kafka Connect Separate cluster, scaling, updates MSK Connect, Confluent Connectors
Schema Registry Apicurio, Karapace Confluent Schema Registry, AWS Glue
Replication slots Extra I/O and disk space on legacy DB —
Event storage Disks + retention policy Payment for volume and throughput

Rule: self-hosted is beneficial at stable load with an SRE team. Managed — at variable load and desire to cut operational costs. As throughput grows, managed may become more expensive than self-hosted — calculate cost per 1M events.

• • •

IXCDC Pipeline Observability: What to Monitor

A CDC pipeline is not "set and forget." Without monitoring, you will learn about a problem only when the WAL slot overflows and the legacy DB crashes.

Kafka Connect metrics

  • source-record-poll-rate — how many records the connector reads from the DB;
  • source-record-write-rate — how many it publishes to Kafka;
  • poll-batch-avg-time-ms — time to read a batch;
  • offset-commit-success-percentage — offset commit success rate.

Debezium metrics

  • MilliSecondsBehindSource — lag behind the source (in milliseconds);
  • NumberOfEventsFiltered — how many events are filtered;
  • NumberOfCommittedTransactions — number of committed transactions.

PostgreSQL replication slot lag

SELECT slot_name,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag
FROM pg_replication_slots
WHERE slot_name = 'debezium_orders';

If lag grows — the connector is falling behind. If lag exceeds disk size — the DB will crash. The alert should fire when lag > 1 GB or on growth within 15 minutes.

Rule of Three Alerts

Minimum three alerts per CDC pipeline: (1) connector lag > threshold, (2) errors in Kafka Connect, (3) replication slot lag growth. Without any of them you work blind.

• • •

XTesting the CDC Pipeline

CDC is harder to test than regular code: you need a real DB, a real broker, a real connector. The solution — Testcontainers.

  1. Testcontainers with PostgreSQL — spin up a container with wal_level=logical, connect Debezium, verify the pipeline end-to-end.
  2. Duplicate check on restart — stop the connector, change data, start again. Verify the consumer correctly handles duplicates.
  3. Hard delete check — delete a row, verify Debezium publishes an event with op: "d".
  4. Schema evolution check — add a column in the DB, verify Schema Registry accepts the new schema or rejects the incompatible one.
  5. Tombstone handling check — verify the sink correctly deletes the record on tombstone.
• • •

XIPractical Scenario: A 6-Month Migration Path

The article describes patterns, but what does a real path look like? Here is a typical scenario for a PostgreSQL monolith that needs to be integrated into an event-driven architecture.

Month 1–2

Inventory and Preparation

Identify which legacy DB tables contain data needed by other services. Configure wal_level=logical, create a replication slot, verify lag monitoring. Install Kafka + Schema Registry + Kafka Connect. Set up Debezium on 1–2 pilot tables.

Month 3–4

Eventification

Deploy Kafka Streams / Flink pipeline to denormalize raw CDC streams into canonical business events. Register event schemas in Schema Registry with BACKWARD_TRANSITIVE mode. Connect the first consumers — let them read canonical streams instead of direct monolith DB queries.

Month 5

Strangler Fig for Data

New services are created as consumers of canonical streams, not as clients of the legacy DB. Direct monolith DB queries are gradually redirected to stream reads (via materialized projections). The monolith gets the Outbox pattern for new features.

Month 6

Ownership Transfer

The monolith team officially becomes an "event producer" — responsible for the schema, quality, and publication SLA. Direct monolith DB queries from outside are forbidden by the architectural committee. The monolith starts consuming its own events (event sourcing for new features).

This is not a linear process — each phase may iterate. But the direction is exactly this: from raw CDC streams to canonical events, from direct queries to subscriptions, from a "closed DB" to an "open stream of facts."

• • •

XIICultural Shift: From Pull to Event-First

If you just set up CDC connectors to "pump" data from the monolith to Kafka and stop there, you haven't built an event-driven architecture. You've just built a more complex ETL process. The monolith remains a monolith, and teams won't learn to think in terms of events.

The true goal of Data Liberation is not just moving bytes but changing the culture of data ownership. The team that owns the monolith must recognize itself as an event producer. They are responsible for the quality, schema, and semantics of what they publish.

Over time, as refactoring progresses, the monolith should begin to first generate events, and only then (if it needs to) consume them back to materialize its own local state. The event broker becomes not just a "pipe" for legacy data but a true center of the enterprise nervous system.

• • •

XIIILink to the Series and Synthesis

This is the fourth chapter in the event-driven architecture series. It builds on the three previous ones.

Chapter Focus Link to This Chapter
I. The Medium Shapes the Architecture Why EDA and how it changes organizations The cultural shift during Data Liberation is a direct consequence of Conway's Law
II. The Anatomy of EDA How it works under the hood Consumer groups, offsets, idempotency — all apply to CDC consumers
III. The Anatomy of an Ideal Event How to design contracts Schema Registry and schema evolution are critical for CDC from legacy
IV. Data Liberation How to connect legacy to EDA Eventification as a bridge between the relational model and business events

Key Takeaways of the Chapter

  1. Dual Write — a fundamental problem solved by all three patterns in different ways. Ignoring it means building on an unreliable foundation.
  2. CDC (Debezium) — technically the best extraction method, but it produces raw CRUD events, not business facts. Eventification is a mandatory next step.
  3. Transactional Outbox — the only pattern that allows publishing denormalized business events with atomicity guarantees, but it requires code changes.
  4. Snapshot + Backfill — mandatory steps to fill new streams with history. Without them, new consumers won't see the past.
  5. Schema Registry + CDC — a critical combination: without it, DDL changes in legacy DB silently break consumers.
  6. Sink writes — non-trivial: idempotent writes, tombstone handling, and ordering guarantees must be thought through.
  7. Observability — minimum three alerts: connector lag, Kafka Connect errors, replication slot lag growth.
  8. Migration strategy — gradual (strangler fig), from pilot tables to canonical streams, with ownership transferred to the producer team.

Pre-Production CDC Checklist

  • wal_level=logical (PostgreSQL) or binlog_format=ROW (MySQL) configured
  • Separate replication slot created per connector
  • Replication slot lag monitoring with alert configured
  • Schema Registry connected, compatibility mode — BACKWARD_TRANSITIVE
  • Eventification set up: CDC streams transformed into canonical business events
  • Pipeline tested via Testcontainers, including hard deletes and schema evolution
  • Sink writes idempotent, handle tombstones, and preserve order
  • DDL changes in legacy DB coordinated with consumer teams
  • Backfill of historical data performed for new consumers
  • The monolith team recognizes itself as an event producer responsible for quality
Final Thesis

Legacy system integration is the most difficult but inevitable stage of maturity for any EDM architecture. Technology is only a means. The main criterion for success is the organization's ability to move from the model of "closed databases" to the model of "open streams of facts," where every system respects the boundaries of others and communicates through clearly defined, denormalized event contracts.

Chapter IV of the "Event-Driven Architecture" series. Sources: Debezium, Confluent, Microservices.io, Baeldung, Habr, Conduktor, Microsoft Azure Architecture Center, production system case studies.