<!-- Canonical: https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium
Language: es
Content-SHA256: 9ee9338a39b64c3d618108da28706c627e4662dc6c24928ec5c58f91b6bb2acc
-->

Databases

# PostgreSQL CDC con Debezium: Replicación Lógica, Decodificación WAL y Arquitectura Event-Driven Real

2026-01-133 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/postgres-logical-replication-cdc-debezium>)

## Funcionamiento de la Decodificación del WAL

El Write-Ahead Log (WAL) de PostgreSQL registra cada cambio a la base de datos a nivel físico. La replicación lógica agrega una capa de decodificación que traduce estos cambios físicos en operaciones lógicas.

```sql
-- 1. Habilitar replicación lógica en postgresql.conf
-- wal_level = logical
-- max_replication_slots = 10

-- 2. Crear una publicación (qué tablas capturar)
CREATE PUBLICATION my_publication FOR TABLE
 orders, order_items, customers
 WITH (publish = 'insert, update, delete');

-- 3. Crear un replication slot (cursor del consumidor)
SELECT pg_create_logical_replication_slot(
 'debezium_slot', 'pgoutput'
);

-- Monitorear lag del slot
SELECT slot_name,
 pg_size_pretty(pg_wal_lsn_diff(
 pg_current_wal_lsn(), confirmed_flush_lsn
 )) AS lag_pretty
FROM pg_replication_slots
WHERE slot_type = 'logical';
```

## El Patrón Transactional Outbox

Este es el patrón que cambia fundamentalmente el enfoque de sistemas event-driven con bases de datos relacionales. En vez de intentar escribir atómicamente tanto a Postgres como a Kafka, escribirs el evento a una tabla outbox dentro de la misma transacción:

```sql
CREATE TABLE outbox (
 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
 aggregate_type VARCHAR(255) NOT NULL,
 aggregate_id VARCHAR(255) NOT NULL,
 event_type VARCHAR(255) NOT NULL,
 payload JSONB NOT NULL,
 created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

BEGIN;
 UPDATE orders SET status = 'shipped', shipped_at = now()
 WHERE id = 9001;

 INSERT INTO outbox (aggregate_type, aggregate_id, event_type, payload)
 VALUES (
 'Order', '9001', 'OrderShipped',
 '{"order_id": 9001, "shipped_at": "2026-02-28T10:30:00Z",
 "tracking_number": "1Z999AA10123456784"}'::jsonb
 );
COMMIT;
-- Ambos tienen éxito o ambos fallan. Sin problema de dual-write.
```

Debezium captura el insert al outbox y lo rutea al topic de Kafka apropiado. La tabla outbox se puede truncar periódicamente porque Debezium ya capturó los eventos.

## Gotchas de Producción

### Columnas TOAST y eventos UPDATE

PostgreSQL almacena valores de columnas grandes en tablas TOAST. Cuando hacés UPDATE de una fila pero no se modifica una columna TOASTed, el registro WAL no incluye el valor TOAST sin cambios. Debezium ve ` null `.

```sql
-- Solución: REPLICA IDENTITY FULL
ALTER TABLE orders REPLICA IDENTITY FULL;
```

### Cambios de schema rompen todo

Agregar una columna a una tabla capturada puede causar errores de mismatch de schema. Siempre agregar con un default primero.

### Bloat del slot durante mantenimiento

Durante pg\_dump o vacuum full, el LSN no avanza. Si tu backup tarda 4 horas y teners 10K escrituras/segundo, son ~144M registros WAL retenidos.

## La Arquitectura Final

```
┌─────────────────┐ ┌──────────────┐ ┌───────────────┐
│ PostgreSQL │ │ Debezium │ │ Kafka │
│  │ WAL │ (Kafka │ │ │
│ orders ├────→│ Connect) ├────→│ domain.events │
│ outbox │ │ │ │ .Order │
│ order_items │ │ Transforms: │ │ .Customer │
│ │ │ - Outbox │ │ .OrderItem │
└─────────────────┘ │ - Route │ └───────┬───────┘
  └──────────────┘ │
   ┌──────┴──────┐
   │ Consumers │
   │ - Search │
   │ - Analytics │
   │ - Notify │
   └─────────────┘
```

El patrón outbox maneja eventos de dominio. CDC directo maneja actualizaciones de vistas materializadas para índices de búsqueda y analytics. Los dos patrones se complementan, outbox para eventos intencionales, CDC para sincronización de datos.

Latencia end-to-end total desde commit en Postgres hasta consumer en Kafka: p50 = 45ms, p99 = 180ms. Para un sistema event-driven construido encima de una base de datos monolítica, ese es números son más que aceptables.

## Preguntas frecuentes

### ¿Qué es Change Data Capture (CDC) y por qué usarlo con PostgreSQL?

CDC captura cambios a nivel de fila (inserts, updates, deletes) de una base de datos y los transmite como eventos. Con PostgreSQL, Debezium usa replicación lógica para leer el Write-Ahead Log (WAL), lo que significa cero impacto en queries de la aplicación y sin overhead de polling. Habilita arquitecturas event-driven en tiempo real sin modificar código de la aplicación.

### ¿Qué son los replication slots lógicos de PostgreSQL?

Los replication slots son cursores del lado del servidor que trackean la posición de un consumidor en el WAL. Garantizan que PostgreSQL retiene segmentos WAL hasta que el consumidor confirma el procesamiento, previniendo pérdida de datos. La desventaja es que un consumidor detenido puede causar acumulación de WAL y agotamiento de disco.

### ¿Cuándo debería usar el patrón transactional outbox?

Utilizar el outbox cuando se necesita actualizar tu base de datos y publicar un evento de forma atómica. En vez de dual-write a Postgres y Kafka, escribirs el evento a una tabla outbox dentro de la misma transacción, y Debezium lo captura vía CDC. Esto garantiza semántica exactly-once entre tu base de datos y message broker.

## Tips

1. **Tip:** Siempre colocar ` slot.drop.on.stop=false ` en conectores Debezium de producción. Si accidentalmente se elimina el replication slot, Postgres va a reciclar los segmentos WAL, y se va a necesitar un re-snapshot completo de todas las tablas. Esta es una lección que comúnmente se aprende de la peor manera durante incidentes nocturnos.
2. **Tip:** Monitorear ` pg_replication_slots.confirmed_flush_lsn ` vs ` pg_current_wal_lsn() `. Si la brecha sigue creciendo, tu consumer está quedándose atrás y Postgres está reteniendo segmentos WAL, eventualmente se va a quedar sin disco. Configurar una alerta cuando el lag supere 1 GB.
3. **Tip:** El patrón transactional outbox con Debezium es la única forma confiable de publicar eventos de dominio desde una transacción PostgreSQL. Dual-writes a Postgres y Kafka van a tener *siempre* una ventana de falla donde uno tiene éxito y el otro no. El outbox elimina esto completamente.
4. **Tip:** Utilizar ` publication.autocreate.mode=filtered ` y listar las tablas explícitamente. El default ` all_tables ` va a capturar todas las tablas incluyendo tablas temporales, logs de auditoría, y esa tabla de migración olvidada, inundando tus topics de Kafka con ruido.

- postgresql
- cdc
- debezium
- kafka
- replicación-lógica
- wal
- event-driven
- outbox-pattern

[Volver al Blog](<https://lucioduran.com/es/blog>)
