Change data capture, or CDC, extracts a continuous stream of database changes from the transaction log instead of querying the database. In Postgres, it reads row-level changes from a replication slot as they are committed. In MySQL, it reads from the binlog. The result is an ordered stream of insert, update, and delete events, each tagged with the table, row key, before-image, and after-image.
For feature engineering, CDC makes any operational database a streaming data source without adding query load. Rather than running SELECT queries on a production Postgres instance for recently updated rows, consume changes directly from the replication infrastructure. This makes CDC a common way to feed incremental feature pipelines from transactional systems.
The simplest CDC pattern maps each event to one output row in the feature store, with derived attributes from the source row. A users table containing demographic and account fields produces a user_profile_features table with normalized versions of those fields. Each UPDATE to a source row produces one feature update in the output.
Pattern 1: Direct Row-Level Feature Extraction
Use this pattern when the required feature values come from one source row without aggregation. Deletes are handled by propagating a delete event to the output feature. In Feldera, the pipeline is a simple SELECT with projections and transformations over the CDC source table, plus an UPSERT sink that writes the feature row keyed by entity identifier.
A more common case is calculating aggregate features over CDC source rows. An orders table produces aggregate features per user rather than one feature per order: total orders, total spend, average order value, and recency. Each new order event updates the aggregate for its ordering user.
-- Source: users table via Postgres CDC connector
CREATE TABLE users (
user_id BIGINT PRIMARY KEY,
plan_tier VARCHAR,
country VARCHAR,
updated_at TIMESTAMP
) WITH (connector = 'postgres-cdc', table = 'users');
-- Feature view: always mirrors current row state
CREATE MATERIALIZED VIEW user_profile_features AS
SELECT
user_id,
plan_tier,
country,
updated_at AS profile_last_updated
FROM users;
Feldera implements this as an incrementally maintained materialized view over the CDC source. Each insert or update to the orders table produces a delta update to the user_order_features view, updating the affected user's aggregate in place. Deletes, such as those for cancelled orders, adjust the aggregate accordingly. The feature store receives only updated aggregate rows, not a full recompute of all user aggregates.
Some features enrich a streaming event with data from a reference table that is also changing. An e-commerce click event can be enriched with product metadata to calculate category affinity features. Product metadata changes when products are added, updated, or discontinued. The click stream arrives continuously from a Kafka topic.
Pattern 2: Aggregation Over a CDC Source
This pattern joins two inputs: a high-frequency Kafka event stream and a slowly-changing CDC reference table. A new click joins with the current product metadata for the clicked item. When CDC changes a product metadata row, the join output for recent clicks on that product is updated retroactively with the new metadata. Feldera maintains both sides of this join incrementally.
CDC sources may emit events at uneven rates. A payment table can receive bursts of updates during business hours and very few overnight. Time-windowed features over a CDC source must handle this variation by updating the window aggregate when events arrive rather than on a fixed schedule.
-- Source: orders table via MySQL binlog CDC
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY,
user_id BIGINT,
amount DECIMAL(12,2),
status VARCHAR,
created_at TIMESTAMP
) WITH (connector = 'mysql-cdc', table = 'orders');
-- Incrementally maintained: updates when any order row changes
CREATE MATERIALIZED VIEW user_order_features AS
SELECT
user_id,
COUNT(*) AS total_orders,
COUNT(*) FILTER (WHERE status = 'completed') AS completed_orders,
SUM(amount) FILTER (WHERE status = 'completed') AS lifetime_spend,
MAX(created_at) AS last_order_ts
FROM orders
GROUP BY user_id;
This pattern handles status transitions correctly. An order that moves from pending to completed will fire an UPDATE event in the CDC stream. Feldera retracts the old row's contribution (which was not counted under status = 'completed') and adds the new row's contribution (which now is). The completed_orders count updates without any explicit logic to handle the transition.
Pattern 3: Joining a CDC Source with a Streaming Input
Many feature engineering use cases require enriching a high-frequency event stream with slowly-changing context from a database. Transaction events enriched with merchant category from a merchant table. Clickstream events enriched with user plan tier from a user profile table.
In Feldera, you configure the database table as a CDC connector (the slowly-changing side) and the event stream as a Kafka connector (the high-frequency side), then join them:
CREATE MATERIALIZED VIEW enriched_clicks AS
SELECT
c.event_id,
c.user_id,
c.item_id,
c.event_ts,
u.plan_tier,
u.country
FROM clicks c
JOIN users u ON c.user_id = u.user_id;
The users table is indexed by the incremental engine. When a new click event arrives, it is enriched with the current row from users for the matching user_id. When a user row changes (via CDC), the engine re-evaluates the join for any click events that reference that user_id and emits retractions and additions for the affected output rows.
The performance characteristic to watch here is the write pattern on the database side. If the users table has very high update frequency, every update triggers a re-evaluation of pending join state. For tables that change slowly (less than a few thousand updates per second), this is fine. For high-frequency transactional tables, you may want to apply a filter before the join to limit the scope of re-evaluation.
Pattern 4: CDC-Triggered Time Windows
A common fraud and anomaly detection pattern is: given a change to a record, compute time-windowed features for the entity that changed. A new payment triggers a velocity feature: "how many payments has this account made in the last 10 minutes?"
This pattern combines CDC input with time-windowed aggregation. The CDC stream provides the trigger; the windowed aggregate provides the feature value. In Feldera, this is expressed naturally because the CDC stream and the windowed view are decoupled:
-- Payments CDC source
CREATE TABLE payments (
payment_id BIGINT PRIMARY KEY,
account_id BIGINT,
amount DECIMAL(12,2),
created_at TIMESTAMP
) WITH (connector = 'postgres-cdc', table = 'payments');
-- Time-windowed velocity feature: freshness = CDC propagation lag + compute
CREATE MATERIALIZED VIEW account_payment_velocity AS
SELECT
account_id,
COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '10 minutes') AS payments_10m,
SUM(amount) FILTER (WHERE created_at >= NOW() - INTERVAL '10 minutes') AS amount_10m
FROM payments
GROUP BY account_id;
Operational Considerations for CDC Sources
A few things that come up in production:
Initial snapshot. When you first connect a CDC source, Feldera needs to bootstrap the current state of the table before switching to incremental mode. For large tables, this initial snapshot can be slow. Plan for snapshot time during pipeline initialization; the pipeline will not serve features until the bootstrap is complete.
Replication slot lag in Postgres. Postgres logical replication slots hold WAL segments until the consumer has confirmed receipt. If your Feldera pipeline falls behind due to a restart or a spike in write volume, the replication slot will hold WAL files and can cause disk pressure on the source Postgres instance. Monitor replication slot lag and set replication slot timeout policies that match your operational requirements.
Schema changes. CDC connectors are sensitive to schema changes in the source table. Adding a nullable column with a default is generally handled gracefully, but renaming a column, changing a column type, or dropping a column will break the connector until you update the pipeline definition to match. This is not a limitation specific to Feldera; it is inherent to the CDC mechanism. Build schema migration procedures that update the pipeline definition before the database migration lands.
Idempotency and at-least-once delivery. CDC connectors provide at-least-once delivery in failure cases. Feldera's incremental operators handle duplicate INSERT events correctly for rows with defined primary keys. Always define primary keys on CDC source tables.
Where CDC Is Not the Right Approach
CDC is the right approach when your source of truth is an operational database and you need features that reflect the current state of that database in near-real time. It is not the right approach when your events are already in Kafka (use the Kafka connector directly), when you need features from a data lake or object storage (use the S3 connector), or when your feature freshness requirement is measured in days rather than seconds (a batch export is simpler and cheaper to operate).
CDC also adds operational coupling between your feature pipeline and your source database. A source database outage or failover that changes the replication slot state will affect your feature pipeline. For databases that are heavily loaded or that have maintenance windows during production hours, consider whether the incremental benefit of CDC over a frequent batch export justifies the operational coupling for your specific situation.