DEV Community

Cover image for Stop Treating Your Database Like a Message Queue (And Vice Versa)
turboline-ai
turboline-ai

Posted on

Stop Treating Your Database Like a Message Queue (And Vice Versa)

There is a category of infrastructure mistakes that do not announce themselves loudly. They accumulate quietly, and by the time the pain is obvious, the architecture is load-bearing. Conflating CDC streams with materialized views is exactly that kind of mistake. Teams reach for one when they actually need the other, usually because both feel like they are solving the same problem: "something in the database changed, and I need to know about it."

They are not solving the same problem. Not even close.

What Each Tool Actually Does

A CDC stream captures the fact that a change happened, in order, exactly once. It is an append-only log of events. A row was inserted, updated, deleted. The stream does not care what the database looks like right now. It cares about what happened, and when, and in what sequence. That property, ordering and exactly-once delivery, is the entire point. You can replay it. You can route it. You can fan it out to multiple consumers. It is built for high-volume, low-latency workloads where every event matters independently.

A materialized view is the opposite orientation. It does not care about individual events. It cares about the current answer to a query. It takes an expensive computation and pre-computes it so that reads are fast. Depending on your system, it refreshes on a schedule or incrementally as underlying data changes. The trade-off is that a materialized view is always a snapshot, even a very fresh one, and refreshing it at high frequency starts to cost real money.

The mental model that unlocks the right choice: CDC streams are for reacting to change. Materialized views are for querying the state that change produces.

Where Teams Go Wrong

The most common mistake is using a materialized view as an ingestion layer. Say you want to detect fraud in near real-time as transactions come in. You build a materialized view over the transactions table, refreshing it every 30 seconds, and query it from your detection service. This seems reasonable until it does not. Full refreshes against high-write tables are expensive. You are not actually processing events, you are polling a snapshot. Events that arrive between refreshes get batched together and lose their individual identity. You cannot tell whether two suspicious transactions happened 200ms apart or 25 seconds apart, because the view collapsed them.

The other failure mode is reaching for raw CDC streams where you actually need pre-aggregated state. Say you are building an analytics dashboard that shows revenue by product category, updated every few seconds. You pipe raw CDC events from your orders table into a Kafka topic and have your dashboard consume directly from it. Now every dashboard load requires the frontend or backend to reconstruct current state from an event log. That is not a dashboard problem, that is a stream processing problem you have accidentally handed off to your UI layer.

The Architecture That Actually Works

When the problem is complex enough, both tools belong in the system, doing different jobs.

A practical example: you are running an e-commerce platform and you need low-latency inventory updates across multiple warehouse systems, plus a dashboard that product managers query throughout the day.

The CDC stream, using something like Debezium pulling from Postgres, captures every inventory change event and publishes it to Kafka. Downstream consumers react to those events in real time, updating reservation systems, triggering reorder workflows, sending notifications. Each event is processed once, in order.

# Debezium connector config (simplified)
connector.class: io.debezium.connector.postgresql.PostgresConnector
database.hostname: warehouse-db
database.dbname: inventory
table.include.list: public.stock_movements
topic.prefix: warehouse
Enter fullscreen mode Exit fullscreen mode

Separately, a stream processing job consumes from that same Kafka topic and maintains a materialized view, current stock levels by SKU and location, in a queryable store. The dashboard reads from that materialized view. It gets fast reads and fresh state. The event-driven systems get ordered, individual events.

The CDC stream and the materialized view are not in competition. The stream is the source of truth for what happened. The materialized view is a purpose-built read layer derived from it.

The Decision Is About Consumer Semantics

Before you pick a tool, ask what the consumer needs to do.

If the consumer needs to act on each individual change, trigger something, detect a pattern across events, audit a sequence, then it needs a CDC stream. The identity of each event matters.

If the consumer needs to answer a question about current state, how much stock is left, what is the current order total, what does this user's profile look like right now, then it needs a materialized view. Individual events are noise; the aggregated result is the answer.

In practice, the same underlying change often has two kinds of consumers. Build for both. Use CDC to capture the event log cleanly, and derive materialized state from that log rather than from polling the source directly. That separation keeps your ingestion layer cheap and your read layer fast, without blurring the boundary between the two.

The concrete takeaway: if you find yourself refreshing a materialized view faster than every few seconds to approximate real-time behavior, you have already decided you need a stream. The only question is whether you will admit it.

Top comments (0)