В идеальном мире мы бы начинали каждый IT-проект с чистого листа, проектируя идеальную событийно-управляемую архитектуру с нуля. Но в реальности большинство компаний — это «коричневые поля» (brownfield). У вас уже есть работающие монолиты, реляционные базы данных, унаследованные API и запутанные связи «точка-точка». Переход на микросервисы требует не просто написания нового кода, а интеграции старого с новым.
Ключевым понятием здесь становится освобождение данных (Data Liberation). Это процесс извлечения критически важных бизнес-данных из изолированных хранилищ legacy-систем и публикация их в виде потоков событий. Именно этот шаг превращает базу данных из «личного сейфа» одного монолита в «единый источник истины» для всей организации.
Это четвёртая глава цикла о событийно-управляемой архитектуре. Первая отвечала на вопрос «почему», вторая — «как устроено под капотом», третья — «как проектировать контракты». Теперь мы разберём, как подключить к этому миру legacy-системы, которые никто не собирается переписывать.
Прежде чем разбирать паттерны, нужно понять главную проблему, которую они решают. Когда сервис пытается одновременно записать данные в свою базу и опубликовать событие в брокер (Kafka, RabbitMQ), возникает проблема двойной записи (Dual Write). Эти две операции не могут быть выполнены в одной атомарной транзакции — БД и брокер — разные системы с независимыми транзакциями.
Что может пойти wrong:
Ни один из этих сценариев недопустим для финансового учёта, инвентаризации или логистики. Именно поэтому нужны паттерны, гарантирующие атомарность.
Любая система, которая пытается одновременно обновить БД и опубликовать событие, обречена на рассинхронизацию. Единственный способ её избежать — сделать так, чтобы событие было частью той же транзакции, что и изменение состояния.
Чтобы разорвать спагетти синхронных вызовов и прямых запросов к чужим базам данных, нужно научиться генерировать события на основе изменений в legacy-системах. Существует три основных архитектурных паттерна.
Самый простой, но и самый «грязный» метод. Внешний процесс периодически опрашивает базу данных, выполняя SQL-запросы с фильтрацией по временной метке (updated_at) или автоинкрементному ID.
Работает с любой СУБД без специальной конфигурации. Позволяет сразу денормализовать данные и скрыть структуру таблиц через Views. Не требует прав администратора.
Пропускает hard deletes. Создаёт паразитную нагрузку. Задержка зависит от частоты опроса. Гарантии доставки — at-most-once или at-least-once с двойной обработкой.
Когда выбирать: низкая нагрузка, простая схема, нет жёстких требований к latency, нет hard deletes, нет возможности включить 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 доставка, есть доступ к конфигурации БД, структура таблиц относительно стабильна.
Золотой стандарт для микросервисов. При изменении бизнес-сущности приложение в одной атомарной транзакции обновляет основную таблицу и записывает факт этого изменения в специальную «исходящую таблицу» (Outbox). Отдельный процесс (Relay) забирает данные из Outbox и публикует их в брокер событий.
Гарантирует согласованность — Dual Write решена. Позволяет публиковать богатые денормализованные бизнес-события. Полностью изолирует внутреннюю модель БД от внешнего мира.
Требует изменения кода приложения. Для legacy с закрытым кодом неприменим. Добавляет накладные расходы на транзакции БД. Требует выбора механизма 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.
Одна из самых частых ошибок при внедрении 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
CDC решает задачу real-time доставки изменений, но не отвечает на вопрос: что делать с историческими данными, которые уже есть в legacy-БД? Новый потребитель, подписавшийся на канонический поток сегодня, не увидит события за прошлый год, если их никто не «переиграл».
При первом подключении к таблице Debezium выполняет initial snapshot — читает всю таблицу и публикует по одному событию на каждую строку. Это позволяет новым потребителям получить полное состояние на момент старта.
Полное состояние на момент старта. Не нужно писать отдельный backfill-скрипт. Debezium сам переключается на streaming после завершения.
Нагрузка на legacy-БД (полное чтение таблицы). Может занять часы для больших таблиц. Блокирует replication slot на время snapshot.
Если в legacy-БД есть audit_log или event_store с историческими изменениями, их можно использовать как источник для backfill канонических потоков. Это отдельная задача, отличная от CDC:
occurred_at.Для таблиц с миллионами строк используйте incremental snapshot (Debezium Signal Table): можно запускать snapshot по частям, не блокируя БД. Сигнал передаётся через специальную таблицу, а Debezium выполняет snapshot в фоне, продолжая streaming.
Debezium автоматически генерирует схемы событий из структуры таблиц. Если в legacy-БД кто-то добавляет, переименовывает или удаляет колонку — меняется схема события, и потребители могут сломаться.
Это особенно болезненно, потому что DDL-изменения в legacy часто происходят вне цикла релиза микросервисов — DBA может выполнить миграцию в maintenance window, не уведомив команды потребителей.
Решение — связка Debezium + Schema Registry:
Однако это создаёт дилемму: коннектор остановился → backlog растёт → WAL-слот в PostgreSQL переполняется. Поэтому DDL-изменения в legacy-БД, подключённой к CDC, должны быть согласованы — как релиз микросервиса, с уведомлением и планированием.
Освобождение данных — это только полдела. Часто возникает ситуация, когда legacy-системе (или аналитическому озеру данных) нужно получить данные из нового событийного мира. Здесь на сцену выходит паттерн Sink (Приемник).
Специальный коннектор (Kafka Connect JDBC Sink, Elasticsearch Sink, S3 Sink) подписывается на поток событий, материализует его и аккуратно вставляет/обновляет данные в целевое хранилище.
Но sink-запись в реляционную БД имеет свои подводные камни:
INSERT ... ON CONFLICT DO UPDATE (upsert) вместо голого INSERT.DELETE FROM table WHERE id = ?. Без этого удалённые записи «воскрешаются» при перезапуске коннектера.OrderUpdated придёт раньше OrderCreated (из-за рестарта или rebalance), upsert перезатрёт корректное состояние некорректным. Решение — партиционирование sink-потока по тому же ключу, что и источник, и один consumer на партицию.consumer.max.poll.records=1 и синхронная запись, ценой throughput.| Инструмент | Когда выбирать |
|---|---|
| Confluent JDBC Sink | Managed, простая настройка, стандартные сценарии |
| Debezium JDBC Sink | Обратная запись из Kafka в БД с сохранением семантики Debezium-событий |
| Materialize / Rockset | Streaming materialization — непрерывные SQL-запросы над потоками |
| Custom consumer | Специфичная логика трансформации, недоступная в стандартных коннекторах |
Выбор инструмента 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 | 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 событий.
CDC-пайплайн — это не «настроил и забыл». Без мониторинга вы узнаете о проблеме только когда WAL-слот переполнится, и legacy-БД упадёт.
source-record-poll-rate — сколько записей читает коннектор из БД;source-record-write-rate — сколько публикует в Kafka;poll-batch-avg-time-ms — время на чтение батча;offset-commit-success-percentage — успешность коммитов offset.MilliSecondsBehindSource — отставание от источника (в миллисекундах);NumberOfEventsFiltered — сколько событий отфильтровано;NumberOfCommittedTransactions — число закоммиченных транзакций.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. Без любого из них вы работаете вслепую.
CDC сложнее тестировать, чем обычный код: нужна реальная БД, реальный брокер, реальный коннектор. Решение — Testcontainers.
wal_level=logical, подключить Debezium, проверить pipeline end-to-end.op: "d".Статья описывает паттерны, но как выглядит реальный путь? Вот типичный сценарий для монолита на PostgreSQL, который нужно интегрировать в событийную архитектуру.
Определить, какие таблицы legacy-БД содержат данные, нужные другим сервисам. Настроить wal_level=logical, создать replication slot, проверить мониторинг lag. Установить Kafka + Schema Registry + Kafka Connect. Настроить Debezium на 1–2 пилотных таблицах.
Разворачивать Kafka Streams / Flink pipeline для денормализации сырых CDC-потоков в канонические бизнес-события. Зарегистрировать схемы событий в Schema Registry с режимом BACKWARD_TRANSITIVE. Подключить первых потребителей — пусть читают канонические потоки вместо прямых запросов к БД монолита.
Новые сервисы создаются как потребители канонических потоков, а не как клиенты legacy-БД. Прямые запросы к БД монолита постепенно переводятся на чтение из потоков (через материализованные проекции). Монолит получает Outbox-паттерн для новых фич.
Команда монолита официально становится «производителем событий» — отвечает за схему, качество и SLA публикации. Прямые запросы к БД монолита извне запрещены на уровне архитектурного комитета. Монолит начинает потреблять собственные события (event sourcing для новых фич).
Это не линейный процесс — каждая фаза может итерироваться. Но направление движения именно такое: от сырых CDC-потоков к каноническим событиям, от прямых запросов к подпискам, от «закрытой БД» к «открытому потоку фактов».
Если вы просто настроите CDC-коннекторы, чтобы «перекачать» данные из монолита в Kafka, и на этом остановитесь, вы не построили событийную архитектуру. Вы просто построили более сложный ETL-процесс. Монолит останется монолитом, а команды не научатся мыслить категориями событий.
Истинная цель Data Liberation — не просто переместить байты, а изменить культуру владения данными. Команда, владеющая монолитом, должна осознать себя как производителя событий. Они несут ответственность за качество, схему и семантику того, что они публикуют.
Со временем, по мере рефакторинга, монолит должен начать сначала генерировать события, и только затем (если ему самому это нужно) потреблять их обратно для материализации своего локального состояния. Брокер событий становится не просто «трубой» для legacy-данных, а настоящим центром нервной системы предприятия.
Эта глава — четвёртая в цикле о событийно-управляемой архитектуре. Она опирается на три предыдущие.
| Глава | Фокус | Связь с текущей главой |
|---|---|---|
| I. Среда формирует архитектуру | Почему EDA и как это меняет организации | Культурный сдвиг при Data Liberation — прямое следствие закона Конвея |
| II. Анатомия EDA | Как устроено под капотом | Consumer groups, offsets, идемпотентность — всё это применимо к CDC-потребителям |
| III. Анатомия идеального события | Как проектировать контракты | Schema Registry и эволюция схем — критичны для CDC из legacy |
| IV. Data Liberation | Как подключить legacy к EDA | Событизация как мост между реляционной моделью и бизнес-событиями |
wal_level=logical (PostgreSQL) или binlog_format=ROW (MySQL)BACKWARD_TRANSITIVEИнтеграция legacy-систем — это самый сложный, но неизбежный этап зрелости любой EDM-архитектуры. Технология — лишь средство. Главный критерий успеха — способность организации перейти от модели «закрытых баз данных» к модели «открытых потоков фактов», где каждая система уважает границы других и общается через чётко определённые, денормализованные контракты событий.
Глава IV цикла «Событийно-управляемая архитектура». Источники: Debezium, Confluent, Microservices.io, Baeldung, Habr, Conduktor, Microsoft Azure Architecture Center, практические кейсы production-систем.
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.
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:
None of these scenarios is acceptable for financial accounting, inventory, or logistics. This is why patterns that guarantee atomicity are needed.
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.
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.
The simplest but "dirtiest" method. An external process periodically polls the database, executing SQL queries filtered by timestamp (updated_at) or auto-increment ID.
Works with any DBMS without special configuration. Allows immediate denormalization and hiding table structure via Views. No admin rights required.
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.
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.
Low latency (milliseconds). Captures everything, including hard deletes. Doesn't load the DB with queries. Preserves transaction order.
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.
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.
Guarantees consistency — Dual Write is solved. Allows publishing rich, denormalized business events. Fully isolates the internal DB model from the outside world.
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.
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.
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.
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
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.
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.
Full state at start moment. No need for a separate backfill script. Debezium switches to streaming after completion.
Load on the legacy DB (full table read). Can take hours for large tables. Blocks the replication slot during snapshot.
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:
occurred_at.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.
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:
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.
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:
INSERT ... ON CONFLICT DO UPDATE (upsert) instead of a raw INSERT.DELETE FROM table WHERE id = ?. Otherwise, deleted records "resurrect" on connector restart.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.consumer.max.poll.records=1 and synchronous writes, at the cost of throughput.| 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 |
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. |
| 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.
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.
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.MilliSecondsBehindSource — lag behind the source (in milliseconds);NumberOfEventsFiltered — how many events are filtered;NumberOfCommittedTransactions — number of committed transactions.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.
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.
CDC is harder to test than regular code: you need a real DB, a real broker, a real connector. The solution — Testcontainers.
wal_level=logical, connect Debezium, verify the pipeline end-to-end.op: "d".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.
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.
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.
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.
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."
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.
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 |
wal_level=logical (PostgreSQL) or binlog_format=ROW (MySQL) configuredBACKWARD_TRANSITIVELegacy 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.