The four questions that choose a serving store, when the warehouse is enough, rollups before serving, Pinot, Druid and ClickHouse, updates and repeats in a fast store, late events, and what freshness costs.
⏱ 11 min readTopics chapter readerLevel · Streaming & Real-Time
01 · Overview
Start here
⏱ 5 min · Topic 1 of 10
A stream job ends by writing results to a sink. But people do not read a sink. They read a dashboard, an app screen or an API.
The serving layer is the store that answers those reads. This chapter shows how to choose it for data that must be fresh.
What you will learn
What a serving layer must do, and how to measure it.
When the warehouse is enough, and when it is not.
Why you add up rows before you serve them.
What Apache Pinot, Apache Druid and ClickHouse are for.
How a fast store handles updates and repeats.
How late events and freshness work at the serving layer.
What each step of freshness costs.
The example is an operations dashboard for a shop with many stores. It shows orders and revenue per store, per minute. All numbers are made up.
02 · The job
What the serving layer must do
⏱ 5 min · Topic 2 of 10
Before you choose a store, ask four questions. Write the answers down, because they decide the design:
Freshness: how old may the newest number be? One day, 15 minutes, one minute?
Speed: how long may one query take? Ten seconds for an analyst, under one second for a screen.
Load: how many queries per second, at the busiest time?
Shape: are the queries the same few each time, or new ones each time?
Three kinds of reader give very different answers:
Reader
Freshness
Speed
Load
Shape
An analyst asking new questions
Hours is fine
Seconds to minutes
A few queries
New each time
An operations dashboard for 200 staff
Under one minute
Under 2 seconds
Tens per second
The same tiles
Numbers inside your app, for every customer
Seconds
Under 100 milliseconds
Thousands per second
The same few, per customer
The warehouse fits the first reader well. The third reader almost always needs a different store. The second is the common interview case, and the answer depends on the numbers.
03 · Start simple
When the warehouse is enough
⏱ 8 min · Topic 3 of 10
Start with the warehouse you already have. A stream or a micro-batch job writes there every minute or so, and the dashboard reads it.
Freshness is usually fine. The problem is load: many people, refreshing at once.
Count the queries. Use this arithmetic for the operations dashboard:
How many queries run at once? (made-up dashboard)
Step
Number
People watching
200
Tiles on the dashboard
6
Each tile refreshes every
30 seconds
Queries per second: 200 × 6 ÷ 30
40
Time per query on the raw orders table
2 seconds
Queries running at once: 40 × 2
80
The last line is a simple rule: queries at once = queries per second × seconds per query.
By default, one Snowflake warehouse cluster runs at most 8 queries at once. The others wait in a queue. So 80 at once needs about 10 clusters, or a long queue.
Before you buy a faster store, try three cheaper fixes:
Make each query cheap. Read a small table of totals, not raw orders. The next section shows how.
Share one answer. All 200 people see the same tiles, so run each tile once per refresh and send the result to everyone. That is 6 queries every 30 seconds, not 40 per second.
Refresh less often. A tile that changes once a minute does not need a refresh every 30 seconds.
With all three fixes, the warehouse is often enough for an internal dashboard.
Interview question
When you hear "real-time dashboard", ask how many viewers and how fresh, before naming a tool. Then do this arithmetic out loud. It shows you choose a store from numbers, not from habit.
Store A at 10:00 has orders 201 and 203: 20.00 + 5.00 = 25.00. At 10:01 it has 204 and 207: 12.00 + 3.00 = 15.00.
Here eight rows become five. At real scale the gap is much bigger.
Say the shop has 3,000 orders per second and 500 stores. That is 180,000 orders a minute, but at most 500 rows a minute in store_minute.
You can compute the totals in three places:
Where
How
Good when
In the stream job
A one-minute window in Flink or Spark, written to the store
Several stores or apps need the same totals
When the store loads the data
Druid rollup, or a ClickHouse materialized view
The fast store reads straight from Kafka
In the warehouse
A table or view refreshed every minute
The warehouse is enough for the load
Watch out
A rollup throws away detail. If the tile later needs "orders over 100.00", the per-minute table cannot answer it. Keep the raw events somewhere cheap, such as the lake, so you can rebuild the rollup.
05 · Fast stores
Pinot, Druid and ClickHouse
⏱ 6 min · Topic 5 of 10
Some reads need under one second at thousands of queries per second. For those, teams use a real-time OLAP store. OLAP means online analytical processing: queries that filter and add up many rows.
These stores read straight from Kafka. New rows can be queried within seconds. They keep data in columns, with indexes built for filters and totals.
The three common open-source ones started inside companies with this exact problem:
Store
Started at
Loads from Kafka
Known for
Updates
Apache Pinot
LinkedIn
Yes, as a real-time table
Analytics inside a product, for many users at once
Upserts by primary key
Apache Druid
Metamarkets
Yes, with a Kafka supervisor
Event data by time, with rollup when it loads
No update of one row. Rewrite the data for a time range.
ClickHouse
Yandex
Yes, with the Kafka table engine
Very fast SQL on one table, flexible queries
Special table engines that remove repeats later
All three are also sold as managed services. Running one yourself is a real cost: another cluster to size, upgrade and watch.
So a fast store is a step you take for a reason. Name the reason: the load, the speed, or numbers inside your product.
Which store for which reader?
Reader
A good first choice
An analyst asking new questions
The warehouse
An internal dashboard, tens of queries per second
The warehouse, with a rollup and a shared cache
Numbers inside your app, thousands of queries per second
A real-time OLAP store, such as Pinot or ClickHouse
One value per key, such as "this driver's rating"
A key-value store, such as Redis or DynamoDB
06 · Corrections
Updates and repeats in a fast store
⏱ 9 min · Topic 6 of 10
Totals change. A late order arrives, or a stream job restarts and sends a total twice. The serving store must end with the right number.
Fast stores are built for adding rows, not changing them. Each one handles this in its own way.
ClickHouse: the newest version wins, later
In ClickHouse, a ReplacingMergeTree table keeps one row per sort key in the end. With a version column, it keeps the highest version.
Here a late order, 209, adds 6.00 to store A at 10:01. The stream job sends a new total with version 2:
Two versions of the same minuteworked example
SQL
Input data
Rows written for store A at 10:012 rows
minute
store_id
orders
revenue
version
10:01
A
2
15.00
1
10:01
A
3
21.00
2
CREATETABLEstore_minute(minuteDateTime,store_idString,ordersUInt32,revenueDecimal(10,2),versionUInt64)ENGINE=ReplacingMergeTree(version)ORDERBY(store_id,minute);-- Two queries for store A at 10:01:SELECTorders,revenueFROMstore_minuteWHEREstore_id='A'ANDminute='2026-01-22 10:01:00';SELECTorders,revenueFROMstore_minuteFINALWHEREstore_id='A'ANDminute='2026-01-22 10:01:00';
What each query returns, before a background merge
Query
Rows returned
Without FINAL
2 rows: (2, 15.00) and (3, 21.00)
With FINAL
1 row: (3, 21.00)
Without FINAL, a SUM over this minute gives 36.00, not 21.00. The old row goes away only when ClickHouse merges the files, at a time you do not control.
FINAL gives the right answer, but each query does more work. Another way is to pick the newest row in the query, for example with argMax(revenue, version).
Pinot and Druid
Pinot can upsert by primary key on a real-time table. The newest record wins, chosen by the time column unless you set another column.
Pinot needs the Kafka topic to be partitioned by that primary key. Then every version of one key reaches the same server.
Druid has no update of one row by key. To correct data, you rewrite the data for a time range, such as one hour, from the source.
Watch out
A fast store that removes repeats "later" will show double totals for a while. Test the dashboard query with a repeated row before launch. Then choose FINAL, an upsert table or a query that picks the newest row.
07 · Time
Late events and freshness
⏱ 6 min · Topic 7 of 10
Order 209 was placed at 10:01:40 but arrived at 10:03. By then the dashboard already showed 10:01 for store A as 15.00.
So a minute on the dashboard is not final when it first appears. It can change while late events arrive.
You choose how long a minute may change. The chapter "Stream Processing" calls this the watermark. Here is the store A tile at three moments:
The store A tile for 10:01, over time
Time
The tile shows for 10:01
Why
10:02
2 orders, 15.00
Orders 204 and 207 are counted
10:03
3 orders, 21.00
Late order 209 arrived, and the job sent version 2
10:06
3 orders, 21.00, final
The 3-minute watermark passed the end of 10:01. Later events go to a correction job.
Show freshness on the screen. A line such as "Data as of 10:05:40" tells people how old the numbers are.
Measure it as now minus the newest event time loaded into store_minute. Alert when it passes the promise, for example 2 minutes.
Remember
A dashboard that looks fresh but stopped updating is worse than one that says it is late. Always show the "data as of" time, and alert on it.
08 · Trade-off
Freshness costs money
⏱ 4 min · Topic 8 of 10
Each step to fresher data adds parts that must run all the time. This table shows what each step usually needs:
Freshness
Usual design
What you add
Next morning
A nightly batch into the warehouse
Nothing new
About 15 minutes
A micro-batch every 15 minutes
A scheduler that runs all day
About 1 minute
A stream job into a rollup table
Kafka, a stream job that never stops, a watermark plan
A few seconds, for many users
A stream into a real-time OLAP store
A second store to run and pay for, upsert or repeat handling
Ask what people do with a fresher number. If nobody acts within a minute, a 15-minute dashboard is cheaper and enough.
Interview question
Interviewers ask: "Does this design meet a 15-minute promise?" Add up each step: the source, the stream, the load and the refresh. Name the slowest one.