Database Talking back? Building a CDC Pipeline with Spring Boot, Embedded Debezium & Postgres
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
-
Start Postgres with logical replication enabled.
-
Apply the schema and publication SQL.
-
Run your Spring Boot app:
./gradlew bootRunor
java -jar build/libs/profile-cdc-*.jar -
Now, update a record in Postgres:
UPDATE public.profiles SET last_logged_in = now() WHERE username = 'janedoe'; -
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
| Issue | Cause | Fix |
|---|---|---|
wal_level not logical | Postgres didn’t reload config | Restart Postgres |
publication does not exist | Name mismatch | Check publication name in Debezium config |
| Offset file permission error | Path unwritable | Update 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. 🔥