Database Talking back? Building a CDC Pipeline with Spring Boot, Embedded Debezium & Postgres

DatabaseSpringbootPostgreSQLData StreamingDebeziumBackend

If you’ve ever needed to track when users log in, sync profile updates to other systems, or trigger actions based on live data then Change Data Capture (CDC) might just become your new best friend.

Instead of constantly polling your database to ask, “Has anything changed yet?”, CDC lets your application listen for changes as they happen. It’s like turning your database into a stream of real-time events.

In this post, we’ll build a local CDC server using Spring Boot and Embedded Debezium, with Postgres as our source of truth.

By the end, you’ll have a working CDC setup that detects whenever a user’s last_logged_in field changes in the profiles table and reacts to it instantly.

Why CDC Is a Game-Changer

Imagine a user logging into your app. Their profile record updates with a new timestamp. Now imagine having multiple services — notifications, analytics, auditing all needing to react to that change.

Without CDC, you might:

  • Schedule batch jobs to sync “recently updated profiles.”
  • Query for diffs every few minutes.
  • Miss real-time opportunities (like sending a “Welcome back!” email instantly).

With CDC:

  • Every update becomes an event.
  • Your app reacts immediately.
  • No polling, no lag, no duplicated logic.

Postgres already stores every database change in its Write-Ahead Log (WAL). Debezium just reads that log, transforms it into structured JSON, and delivers it to your app.

Add a simple change handler, and you’re streaming profile updates in real time no Kafka required.

Setting Up Postgres for Logical Decoding

CDC relies on Postgres’ logical replication feature, which lets us stream data changes.

Let’s set it up properly.

Step 1: Update postgresql.conf

Open your Postgres config file and enable logical replication:

wal_level = logical
max_replication_slots = 10
max_wal_senders = 10
max_connections = 200
wal_keep_size = 256MB

This configures Postgres to expose logical changes and allocate replication resources for Debezium.

Step 2: Configure pg_hba.conf

Allow your CDC user to connect:

host    all             all                 127.0.0.1/32          md5
host    replication     your_cdc_user       127.0.0.1/32          md5

Restart Postgres to apply the changes (brew services restart postgresql, docker restart, or equivalent).

Step 3: Verify the Setup

Check that logical replication is active and your user is ready:

SHOW wal_level;
SELECT rolreplication FROM pg_roles WHERE rolname = 'your_cdc_user';

You should see wal_level = logical and rolreplication = t.


Creating the profiles Table and Publication

Let’s simulate a real-world use case: tracking when users last logged in. The first thing we will need to do is to create a users table where we will keep all of our users profile and stream events from. Use psql to access your database and log in as an admin user

psql -U admin_user

# Run this if the database already exists
psql -U admin_user -d user_data

You will then be prompted for your password, type it in and then create the profiles table

CREATE DATABASE "user_data" IF NOT EXISTS;
\c user_data

CREATE TABLE public.profiles (
    id uuid PRIMARY KEY,
    username text,
    email text,
    last_logged_in timestamptz DEFAULT now()
);

Now create a publication. Publications expose changes on the table:

CREATE PUBLICATION profiles_pub FOR TABLE public.profiles;

When Debezium starts, it will automatically create a replication slot (e.g., profiles_slot) to stream changes.

You can confirm it later like this:

SELECT slot_name, plugin, active FROM pg_replication_slots;

Building the Spring Boot CDC Service

Now that Postgres is ready, let’s make Spring Boot listen to those updates.

Dependencies (build.gradle)

implementation 'io.debezium:debezium-embedded:2.6.0.Final'
implementation 'io.debezium:debezium-connector-postgres:2.6.0.Final'

We’ll use Debezium in embedded mode, which runs entirely inside your Spring Boot process — no external Kafka setup needed.


Core Components

Let’s break the CDC service into three parts:

  • DebeziumConfigFactory — Builds Debezium configuration based on environment variables.
  • DebeziumEngineRunner — Starts the Debezium engine in a background thread.
  • ChangeHandler — Processes incoming CDC events.

Example Debezium Configuration

Configuration.create()
    .with("name", "profiles-connector")
    .with("connector.class", "io.debezium.connector.postgresql.PostgresConnector")
    .with("plugin.name", "pgoutput")
    .with("database.hostname", host)
    .with("database.user", user)
    .with("database.password", password)
    .with("database.dbname", "user_data")
    .with("slot.name", "profiles_slot")
    .with("publication.name", "profiles_pub")
    .with("table.include.list", "public.profiles")
    .with("offset.storage.file.filename", "./offsets.dat")
    .build();

When this engine runs, Debezium will connect to Postgres, listen for changes on the profiles table, and deliver JSON events like:

{
  "before": {
    "id": "1234",
    "last_logged_in": "2025-11-01T08:12:34Z"
  },
  "after": {
    "id": "1234",
    "last_logged_in": "2025-11-03T09:01:17Z"
  },
  "op": "u"
}

That’s your cue that a user just logged in!

Managing Configuration via Environment Variables

Use a .env file to store your database credentials and CDC settings:

DB_HOST=localhost
DB_USER=cdc_user
DB_PASSWORD=supersecret
CDC_SLOT_NAME=profiles_slot
CDC_TABLE_INCLUDE_LIST=public.profiles
CDC_OFFSETS_FILE=./offsets.dat

These values are automatically loaded by Spring Dotenv or Spring Boot’s configuration processor.

Running and Observing the CDC Server

  1. Start Postgres with logical replication enabled.

  2. Apply the schema and publication SQL.

  3. Run your Spring Boot app:

    ./gradlew bootRun
    

    or

    java -jar build/libs/profile-cdc-*.jar
    
  4. Now, update a record in Postgres:

    UPDATE public.profiles
    SET last_logged_in = now()
    WHERE username = 'janedoe';
    
  5. Watch your Spring logs — you’ll see something like:

    CDC Event: UPDATE on public.profiles
    User janedoe just logged in at 2025-11-03T09:01:17Z
    

Troubleshooting Common Issues

IssueCauseFix
wal_level not logicalPostgres didn’t reload configRestart Postgres
publication does not existName mismatchCheck publication name in Debezium config
Offset file permission errorPath unwritableUpdate CDC_OFFSETS_FILE or mount a writable directory

Taking It Further

Once you’ve got basic CDC running, the sky’s the limit.

Here are some ideas:

  • Send login events to Slack or email for analytics.
  • Update an “active users” dashboard in real-time.
  • Pipe CDC events into Kafka or Redis Streams for scalable event distribution.
  • Implement a WebSocket endpoint to broadcast user login activity to your frontend.

Wrapping Up

And that’s it!! You’ve just built a real-time CDC pipeline that reacts whenever a user logs in.

With Postgres as your source, Debezium for change streaming, and Spring Boot orchestrating everything, you now have the foundation for event-driven systems that never miss a beat.

No more stale dashboards. No more sync delays. Just instant, reliable data flow.

Your database just started talking back. 🔥