KStream vs KTable in Kafka
A plain-English guide to the two core ideas of Kafka Streams: a stream of events that never stops, and a table that always shows the latest answer. Learn what each one is, how each one works inside, and exactly when to pick which.
Imagine you run a busy coffee chain. All day long, cash registers send little messages to head office: “Order placed”, “Order paid”, “Order refunded”. Head office wants to answer two very different questions. The first question is, “What exactly happened today, in order?” The second question is, “Right now, how many loyalty points does each customer have?”
Those two questions look similar, but they need two different ways of thinking about the same data. The first is a diary: every entry matters, and nothing is ever crossed out. The second is a scoreboard: only the latest number matters, and new numbers replace old ones. In Apache Kafka, the diary is called a KStream and the scoreboard is called a KTable.
Once you feel this difference in your bones, a huge part of Kafka Streams suddenly becomes simple. Joins, counting, windows, fault tolerance, and even how much disk space you need all follow from this one idea. This guide walks through it slowly, one step at a time, with everyday comparisons and real examples from companies that run Kafka at giant scale.
A KStream treats every record as a new, independent event (an insert). A KTable treats every record as an update to the latest value for its key (an upsert). Same topic underneath, different meaning on top.
The Problem Kafka Streams Solves
Before comparing two tools, we need to know the job they were built for.
What is Kafka, in one minute?
Apache Kafka is a system that stores and moves messages between programs. A message is called a record. Records are saved in a named list called a topic. Think of a topic as a very long notebook where you can only add new lines at the bottom and never erase or edit old lines.
A big topic is split into smaller pieces called partitions, so many machines can share the work. Every record inside a partition gets a number called an offset, which is simply its position in line: first, second, third, and so on. Each record also carries an optional key (like a customer ID), a value (the actual content), and a timestamp.
The key is very important. Kafka uses the key to decide which partition a record lands in. All records with the same key go to the same partition, so their order is preserved. Remember this, because the whole KTable idea leans on it.
A topic is like a school’s attendance register kept by many teachers at once. Each class (partition) has its own register. Teachers only write new lines at the bottom. The student’s roll number is the key, so the same student always appears in the same class register.
Why plain topics are not enough
Storing records is only half the story. Companies also need to react to records as they arrive: count them, filter them, combine them with other data, and raise alarms. You could write this yourself using a plain Kafka consumer and producer, but you would quickly need to build many hard things by hand: remembering counts between messages, surviving crashes without losing those counts, splitting work across machines, and handling messages that arrive late.
Kafka Streams is a library that gives you all of this. It is not a separate server you install. You add it to your normal Java or Kotlin application, and your application becomes a stream processor. Run more copies of the application and the work is shared automatically. Kafka Streams arrived with Kafka version 0.10 in 2016, and it has grown a lot since then.
Two mental models for the same data
Kafka Streams gives you two main ways to look at a topic. It calls them abstractions, which just means “ways of thinking that come with built-in tools”.
KStream
An endless diary of events. Every record is a new fact that happened. Nothing replaces anything.
KTable
A live scoreboard. Each key has exactly one current value. A new record for the same key replaces the old one.
Real-world example: LinkedIn
Kafka was created at LinkedIn around 2010 to move activity data such as profile views, clicks, and messages. Some questions on that data are diary questions (“what did this member click, in order?”) and some are scoreboard questions (“what is this member’s current profile headline?”). Both kinds of question come from the same firehose of records.
The Big Picture: One Topic, Two Views
The most important sentence in this whole article comes first.
A KStream and a KTable are not different kinds of topics. They are two different interpretations of records. The topic is just a notebook of records. Your code decides whether to read that notebook as a diary or as a scoreboard.
Let us use a simple example. A topic called loyalty-points holds these records, in this order, all using the customer name as the key:
- Asha, 10 points
- Ravi, 5 points
- Asha, 25 points
- Ravi, 0 points
- Asha, 40 points
Read as a KStream, this is five separate events. Nothing is forgotten. If you asked “how many records are there?” the answer is five. If you asked for the total of all values, you would add up 10 + 5 + 25 + 0 + 40.
Read as a KTable, this is only two rows. Asha has 40 points. Ravi has 0 points. The earlier values were replaced by later ones. If you asked “how many rows are there?” the answer is two.
flowchart LR
T["Kafka Topic: loyalty-points (5 records)"]
T --> KS["KStream view: all 5 events kept"]
T --> KT["KTable view: latest value per key"]
KS --> R1["Asha 10, Ravi 5, Asha 25, Ravi 0, Asha 40"]
KT --> R2["Asha = 40, Ravi = 0"]
The diagram shows one topic feeding two views. On the left path, the KStream keeps every event, so the result is a list of five facts. On the right path, the KTable keeps only the newest value for each key, so the result is two rows. The topic did not change at all. Only your reading changed.
Think of your bank. Your passbook lists every deposit and withdrawal, in order. That is a KStream. Your current balance is one number that changes every time a new transaction arrives. That is a KTable. Both come from the same transactions, but they answer different questions.
The words “insert” and “upsert”
Database people use two small words that explain everything. An insert adds a brand-new row every time. An upsert is a mix of “update” and “insert”: if the key already exists, it updates that row; if the key is new, it adds a row. KStream behaves like insert. KTable behaves like upsert.
| Question | KStream | KTable |
|---|---|---|
| What does one record mean? | A new event | The newest value for a key |
| Are older records still visible? | Yes, all of them | No, they are replaced |
| Can the same key appear many times? | Yes, freely | Once, as the latest row |
| Does it need to remember things? | Not usually (stateless) | Yes, it keeps a state store |
| Typical example | Clicks, payments, sensor readings | User profile, account balance, price list |
KStream Explained: The Endless Diary
A KStream is the simplest of the two, so we start here.
What it is
A KStream is a sequence of records that never ends, where each record is an independent fact. It is often called a record stream. The word “unbounded” describes it well: there is no last record, because new ones keep arriving as long as the world keeps producing events.
Two records with the same key do not affect each other. If a customer clicks “Buy” three times, that is three events, not one event that changed three times. A KStream respects that.
A KStream is a security camera recording. Every second is saved. You cannot say “the camera only remembers the last frame”. Each frame is its own piece of history.
What you can do with it
Most KStream tools are stateless. That means each record can be handled on its own, without remembering anything about earlier records. This makes them cheap and fast.
Filter
Keep only the records you care about, such as payments above a certain amount.
Map values
Change the content of each record, such as converting currency, without touching the key.
Branch or split
Send records to different paths, such as domestic orders and international orders.
Flat map
Turn one record into several, such as splitting one order into its line items.
Aggregate
Count, sum, or average records grouped by key. The result is a KTable.
Join
Combine events with other streams or tables to add context.
Changing the key of a KStream is a big deal. If you pick a new key, Kafka Streams must reshuffle records so that same-key records meet on the same machine again. This creates an extra internal topic and extra network traffic. Changing only the value is free by comparison.
When a KStream is the right choice
Use a KStream whenever every event matters on its own. Payments, ride requests, page views, temperature readings, and log lines are perfect examples. If losing the history would hurt the business, you want a stream.
Real-world example: Uber
A ride request is an event. It happens once, at a moment in time, and nobody edits it later. Uber runs one of the largest Kafka deployments in the world, and event flows like ride requests, driver location pings, and trip state changes are natural streams. Each ping is a new fact about where a driver is at that moment.
Real-world example: Netflix
Every play, pause, and stop on a Netflix screen is an event. Netflix’s Keystone pipeline has been described in public engineering talks as moving events on a scale of trillions per day, all built on Kafka. Analysts need each event separately to understand viewing behavior.
KTable Explained: The Live Scoreboard
A KTable looks small on the surface but does a lot of hidden work.
What it is
A KTable is a view of a topic where each key has exactly one current value. When a new record arrives with a key you have seen before, the old value is replaced. When a record arrives with a brand-new key, a new row appears. Because of this, a KTable is called a changelog stream: it is a stream of changes that, when applied one after another, build a table.
A KTable is the contacts app on your phone. If a friend changes their number, you edit the entry. You do not keep two entries for the same friend. The app always shows the newest number, and old numbers are gone from view.
Deleting a row: the tombstone
How do you remove a row from a KTable? You send a record with the same key and an empty (null) value. This special record is called a tombstone, like a small grave marker that says “this key is now gone”. The KTable removes the row when it sees it.
Why a KTable needs memory
To know the “latest value for each key”, the application must remember every key it has seen and its current value. Kafka Streams keeps this in a local database called a state store. By default this store is RocksDB, a fast key-value database that lives on the application’s own disk, with hot data cached in memory.
To protect against crashes, every change to the state store is also written to a special Kafka topic called a changelog topic. If the machine dies, another machine can replay that topic and rebuild the exact same table. We explore this in detail later.
Compacted topics: the KTable’s best friend
Normally Kafka deletes old records after a set time. That would be a disaster for a table, because a key that never changes would slowly disappear. To solve this, Kafka offers log compaction. With compaction turned on for a topic, Kafka keeps at least the latest record for every key and cleans away older ones. The topic turns into a compact snapshot of the current state, plus the recent history.
This is why tables and compaction go together so naturally. A compacted topic is basically a table stored on disk in a Kafka-friendly way.
A KTable needs non-null keys. A record with no key cannot say which row it belongs to, so it is skipped when building a table. Always check that the producer sets a key.
When a KTable is the right choice
Use a KTable when you care about the current state of something: a customer’s profile, a product’s price, a driver’s status, an account’s balance, a device’s configuration. You will also use KTables as the result of counting or summing records by key.
Real-world example: The New York Times
The New York Times has publicly described using Kafka as the central source of truth for published content. An article has one current version, but that version changes as editors update it. Systems that read the current article as a table always see the latest text, while the full history stays in the log.
GlobalKTable: The Bonus Third Member
The title says KStream versus KTable, but you will meet a cousin sooner or later.
A normal KTable is split by partition. If a topic has 12 partitions and you run 3 copies of your application, each copy holds roughly a third of the table. This is great for size and speed, but it has a catch: to combine a stream with such a table, both must be split the same way.
A GlobalKTable is different. Every copy of your application loads all partitions of the topic. So every copy has the complete table on its own disk. This gives you two superpowers. First, you can join a stream to it using any field, not just the key. Second, you never worry about matching partition counts.
A normal KTable is like a library where each branch stores only some of the books, and a request must go to the right branch. A GlobalKTable is like giving every branch a full copy of the price list, so any cashier can answer any price question without calling anyone.
Strengths
- Join on any field, not only the record key
- No partition-matching rules to follow
- Lookups are always local and very fast
- New copies of the app get full data immediately after loading
Costs
- Every instance stores the whole table, so memory and disk multiply
- Startup takes longer for big topics
- Only suitable for small or medium reference data
- Updates to it do not trigger new join output on their own
The rule of thumb is simple. Use a GlobalKTable for small, slow-changing reference data such as country codes, currency rates, or product categories. Use a regular KTable for large, fast-changing data such as customers or orders.
Stream-Table Duality: Two Sides of One Coin
This is the idea that makes Kafka Streams feel elegant instead of confusing.
Streams and tables are not rivals. Each can be turned into the other, without losing information. This two-way relationship is called stream-table duality.
From stream to table: aggregation
Take a KStream of events and aggregate it. Aggregating means squeezing many records into a summary, such as counting orders per customer. The result is a KTable, because each customer key now has one current count. Every new event updates that count.
From table to stream: the changelog
Take a KTable and watch every change it goes through. Each change, such as “Asha now has 40 points”, is itself an event. Convert the table into a stream of those changes and you get a KStream again. Kafka Streams offers a simple step for this, and the result is the full history of how the table evolved.
flowchart LR
S1["KStream: order events"] -->|"group by key then count"| T1["KTable: order count per customer"]
T1 -->|"convert to stream of changes"| S2["KStream: count updates"]
S2 -->|"aggregate again"| T2["KTable: rebuilt latest counts"]
Read the diagram from left to right. Order events flow in as a stream. Grouping and counting turns them into a table of counts. Watching that table’s changes gives a new stream of count updates. Applying those updates rebuilds the table. Nothing is lost at any step.
A film is a stream of pictures. Any single picture (a snapshot) is a table: it shows how everything looks at one moment. Play snapshots one after another and you get the film again. The film and the pile of snapshots carry the same story.
Why this matters in practice
Duality explains why Kafka can store a table in a topic (the changelog), why a database can be copied into Kafka (change data capture turns table changes into a stream), and why you can rebuild any table by replaying its stream. It also means you rarely need to worry about picking the “wrong” one forever. You can always convert.
Real-world example: Change data capture at scale
Many companies copy database changes into Kafka. Each row change becomes an event in a stream. Downstream teams then rebuild that database table inside Kafka Streams as a KTable, without ever touching the original database again. The database stays calm, while dozens of teams read the same data in their own shape.
Architecture and Components
Let us open the box and see the parts that make streams and tables work.
The topology: your recipe
A Kafka Streams program is described as a topology, which is a graph of steps. Data enters at source nodes (reading topics), passes through processor nodes (filter, map, aggregate, join), and leaves at sink nodes (writing topics). Think of a factory conveyor belt with stations along the way.
flowchart TD
subgraph Kafka["Kafka Cluster Brokers"]
IN["Input topics"]
REP["Internal repartition topics"]
CL["Internal changelog topics"]
OUT["Output topics"]
end
subgraph App["Streams Application Instance"]
SRC["Source nodes"]
PROC["Processor nodes"]
SS[("State store RocksDB")]
SNK["Sink nodes"]
end
IN --> SRC
SRC --> PROC
PROC SS
PROC --> REP
REP --> SRC
SS --> CL
PROC --> SNK
SNK --> OUT
The diagram shows the two worlds. On the top is the Kafka cluster with four kinds of topics. At the bottom is your application. Records come from input topics into source nodes, flow through processors, and reach sink nodes that write to output topics. Stateful processors, such as a KTable, talk to the state store, and each store change is copied to a changelog topic. When a key changes, records take a detour through a repartition topic and re-enter the source.
Key building blocks
Task
Kafka Streams creates one task per input partition. A task owns its slice of data and its own state store slice.
Stream thread
A thread runs one or more tasks. You choose how many threads each instance runs.
Application instance
One running copy of your program. Add more copies to scale out.
State store
The local database that holds table rows and running aggregates.
Changelog topic
A compacted Kafka topic that copies every state store change.
Repartition topic
An internal topic used when the key changes, so same-key records meet again.
Picture a large post office with many sorting desks. Each desk (task) handles the mail from one pigeonhole (partition). Workers (threads) walk between desks. Each desk keeps a small ledger (state store), and a copy of every ledger line is mailed to head office (changelog topic) in case the desk burns down.
How KStream and KTable fit in
A KStream node is mostly plain plumbing. Records go in, and records come out. A KTable node contains a state store. So the moment you use a KTable, or aggregate a KStream into one, your application becomes stateful, and everything about state (disk, backups, restarts) starts to matter.
| Part | Used by KStream? | Used by KTable? |
|---|---|---|
| Source node | Yes | Yes |
| State store | Only for stateful steps | Always |
| Changelog topic | Only for stateful steps | Yes, or reuses the source topic |
| Repartition topic | When the key changes | When grouping by a new key |
| Sink node | Yes | Yes, after converting to a stream |
Internal Working: A Record’s Journey
Follow one record from the moment it lands in a topic until its result appears.
The record lands in a partition
A producer writes the record. Kafka picks the partition using the key, and the record gets its offset.
The task polls it
The task that owns that partition pulls a batch of records. It chooses records in timestamp order when reading from several partitions, so events stay fairly sorted.
It flows through the processors
Each processor runs in turn. For a KStream step like a filter, the record is simply checked and passed on or dropped.
State is read and updated
For a KTable or an aggregation, the processor looks up the key in the state store, computes the new value, and writes it back.
The change is backed up
The state change is queued for the changelog topic, so another machine could rebuild it after a crash.
Results move forward
The result goes to the next processor or to a sink node that writes to an output topic.
Offsets are committed
Every so often, the application records “I have finished up to offset N” so a restart knows where to continue.
The record cache: why a KTable seems calm
Suppose a customer’s count goes from 1 to 2 to 3 to 4 within a fraction of a second. Sending four separate updates downstream would be wasteful. Kafka Streams keeps a small record cache in front of each state store. Updates to the same key overwrite each other in the cache, and only the latest is sent onward when the cache is flushed, which happens on each commit or when memory fills up.
The result is that a KTable usually emits fewer updates than the number of input records. This surprises many beginners who expect one output per input. That is a feature: a table describes the latest state, so it does not need to announce every tiny step. If you really want every step, you can shrink or disable the cache, at the cost of more traffic.
The time between commits controls how often the cache flushes. A shorter commit interval gives fresher, chattier output. A longer interval gives calmer output and fewer writes.
Processing guarantees
Kafka Streams offers two promises about what happens if something fails mid-flight. At-least-once means no record is lost, but a record might be processed twice after a crash. Exactly-once means the combined effect of reading, updating state, and writing output happens once, even after a crash. Exactly-once works by wrapping the steps in a Kafka transaction. It costs a little speed but is very valuable for money-related tables like balances.
At-least-once is a waiter who, when unsure whether your order reached the kitchen, sends it again. You might get two soups. Exactly-once is a waiter using a numbered ticket system, so the kitchen ignores duplicates.
State Stores, Changelogs, and Fault Tolerance
A table lives on a machine’s disk. What happens when the machine disappears?
The safety net
Every state store has a twin: its changelog topic in Kafka. Because Kafka itself is replicated across several brokers, the changelog is safe even if an application machine dies. The local disk is treated as a fast cache of the real truth, which lives in Kafka.
flowchart LR
A["Instance A owns partition 3"] --> SA[("Local store A")]
SA --> CL["Changelog topic partition 3"]
A -. "crash" .-> X["Instance A lost"]
CL --> B["Instance B takes partition 3"]
B --> SB[("Rebuilt store B")]
STB["Standby replica on Instance C"] -. "keeps warm copy" .-> CL
STB --> B
In the picture, Instance A works on partition 3 and backs up its store into the changelog. When A crashes, Kafka gives partition 3 to another instance. That instance replays the changelog to rebuild the table. If a standby replica was already keeping a warm copy on another machine, recovery is nearly instant because there is very little left to replay.
Failure scenarios and recovery
| What fails | What happens | How to soften it |
|---|---|---|
| One application instance dies | Its tasks move to other instances, which rebuild state from changelogs | Enable standby replicas |
| Local disk is wiped | Full restore from the changelog on restart | Use persistent volumes for state directories |
| A Kafka broker dies | Replicas on other brokers take over, and the app keeps running | Use replication factor of three |
| Very large table restore | Long pause before processing resumes | Standbys, smaller tables, faster disks |
| Source topic data expired | A KTable built from it can lose rows | Use compaction on table source topics |
If a KTable reads from a topic that uses time-based deletion instead of compaction, old keys silently vanish after the retention period. When the application later restarts and rebuilds, those rows are gone forever. Always use compaction for topics that represent tables.
Rebalancing
When an instance joins or leaves, Kafka moves tasks between the remaining instances. This is a rebalance. During a rebalance, moved tasks must restore their state before they can work again. Modern Kafka uses cooperative rebalancing, which moves only the tasks that really need to move, so the rest keep working. Stateless KStream steps rebalance almost instantly, while big KTables take longer.
Real-world example: Rabobank
Rabobank has publicly described using Kafka Streams to power real-time alerts for its customers. Alerts depend on remembering recent account state, so a crashed instance must recover fast. Standby replicas and replicated changelogs are exactly the tools that keep such a service dependable.
Joins: Where Streams Meet Tables
Real questions usually need more than one dataset, and joins are how KStream and KTable finally work together.
The classic example: enriching events
An order event says “customer 42 bought item 7”. That is not very useful for a report. You want the customer’s name and city. The customer data lives in a KTable. Joining the order KStream with the customer KTable adds those fields to each order as it flows past. This is called enrichment, and it is the most common use of the pair.
flowchart LR
O["Orders KStream"] --> J["Stream-Table Join"]
C["Customers KTable"] --> J
J --> E["Enriched orders KStream"]
E --> OUT["Output topic"]
Every time an order arrives, the join looks up the customer’s current row in the table and stitches them together. The important detail is that only the stream side triggers output. If a customer moves to a new city, no new output appears until that customer places another order.
The join menu
| Join type | Meaning | Triggers output when |
|---|---|---|
| KStream to KStream | Match two event streams that happen close together in time | Either side arrives inside the time window |
| KStream to KTable | Add current context to each event | Only when the stream side arrives |
| KStream to GlobalKTable | Same, but using any field and full local copy | Only when the stream side arrives |
| KTable to KTable | Keep a joined table up to date | Either table changes |
| KTable to KTable by foreign key | Join on a field other than the key | Either table changes |
The co-partitioning rule
For the regular joins above, both inputs must be co-partitioned. That means two things: both topics must have the same number of partitions, and records with the same key must have been placed using the same partitioning method. Why? Because the join happens locally inside one task. Customer 42’s orders and Customer 42’s profile must land in the same partition number so the same task sees both.
Two decks of playing cards must be dealt into the same number of piles using the same rule (say, by suit). Only then can each player at a table match a card from deck one with its partner from deck two without walking across the room.
Kafka Streams checks partition counts at startup, but it cannot check the partitioning method. If two producers hash keys differently, the join will quietly miss matches. Use the same key format and the same partitioner everywhere.
Stream-to-stream joins need a window
Two streams never end, so a stream-to-stream join cannot keep all records forever. You give it a time window, such as five minutes. An ad impression and an ad click join only if the click appears within five minutes of the impression. Records outside the window are forgotten to save space.
Real-world example: Pinterest
Pinterest has publicly discussed using Kafka Streams for advertising spend tracking, where events about ads and budgets must be combined quickly and accurately. Combining an event stream with a table of current budgets is the same enrichment idea in a high-stakes setting.
Windows and Time: Counting Within a Slice
Endless streams need boundaries, or answers like “how many?” would never finish.
If you count every click since the beginning of time, the number only goes up. A more useful question is, “How many clicks happened in each five-minute slice?” A window cuts an endless stream into slices of time. Counting inside windows produces a special KTable where the row key is both the original key and the window.
Which clock do we use?
There are two clocks. Event time is when something actually happened, such as the moment a phone tapped “Buy”. Processing time is when your application got around to handling it. On a bad mobile network these can differ by minutes. Kafka Streams mostly uses event time, taken from the record’s timestamp, because that gives the answer people actually want.
Event time is the date stamped on a postcard when it was written. Processing time is the day it finally reaches your mailbox. If you want to know what happened during someone’s holiday, you read the written date, not the delivery date.
Kinds of windows
Tumbling
Back-to-back slices that never overlap, like every hour on the hour. Each event belongs to exactly one window.
Hopping
Fixed-length slices that start more often than they end, such as a ten-minute window that moves every two minutes.
Sliding
Windows defined by how close events are to each other, useful for questions like “any two logins within a minute”.
Session
Windows that grow while activity continues and close after a quiet gap, like one visit to a website.
Late data and the grace period
Sometimes an event arrives after its window has already moved on. Kafka Streams lets you set a grace period, which is extra time to accept late events into a window. Events arriving after the grace period are dropped. A longer grace gives more accurate counts but requires keeping windows open, and therefore state, for longer.
Because a windowed KTable keeps updating until a window closes, downstream systems may see many partial results. If you only want the final answer per window, use the suppress feature, which holds results back until the window is done.
Time and joins
When a stream joins a table, which version of the table row should it see: the version at the time the event happened, or the newest version at the time the event is processed? By default, it uses whatever the table holds right now. Newer Kafka versions added versioned state stores, which keep short history per key so a join can pick the row that was valid at the event’s own timestamp. This helps when events arrive late or out of order.
Real-world example: Ride pricing
A ride-hailing app charges by the price in force when the rider requested the trip, not the price a minute later after a surge change. Joining trip events to a price table with time awareness protects riders from surprises and protects the company from disputes.
Data Flow and the Life of a Key
In Kafka Streams the key is not decoration. It decides where data lives.
Why keys drive everything
Remember that Kafka sends all records with the same key to the same partition. A KTable depends on this. To keep “the latest value per key”, one task must see every update for that key, in order. Because same-key records share a partition, and one task owns each partition, that guarantee comes for free.
The moment the key changes
Now imagine orders arrive keyed by order ID, but you want to count orders per customer. You must re-key the stream by customer ID. After re-keying, records for one customer are scattered across many partitions. Kafka Streams fixes this automatically by writing the records into a repartition topic using the new key and reading them back. This adds one more hop, more network traffic, and more disk.
Records arrive keyed by order
Each order sits in a partition chosen by its order ID.
Your code selects a new key
You choose the customer ID as the key for counting.
Repartition topic is used
Records are written out and read back so same-customer records meet in one partition.
The count is updated
The stateful step reads the customer’s row, adds one, and stores it.
The KTable row is ready
Each customer now has one row showing their current order count.
Records with a null key are dropped from table-building and cannot be joined in key-based joins. Producers should always set meaningful keys. Also, changing the key too many times in one topology multiplies repartition topics and slows everything down.
Key skew: when one key is too popular
If one key receives a huge share of traffic, such as a single celebrity account, that partition and its task become a bottleneck while other tasks sit idle. This is called skew or a hot key. Streams and tables both suffer, but tables suffer more because the state for that key grows in one place. Ideas to soften it include splitting the key into smaller pieces, pre-aggregating before the main step, or choosing a more even key.
Imagine a supermarket where every customer must use the till that matches the first letter of their surname. On a day when everyone is named Sharma, one till has a huge queue while the others are empty.
The lifecycle of a row
A row in a KTable is born when a new key first appears. It is updated each time a fresh record arrives. It dies when a tombstone arrives. Compaction eventually removes old versions and, after a delay, the tombstone itself. Understanding this small life story explains almost every strange table behavior you may meet.
Advantages, Disadvantages, and Trade-offs
Neither view is better. Each is better at something, and each has a price.
KStream
Pros
- Keeps complete history of events
- Stateless steps are cheap, fast, and easy to scale
- Very simple to reason about
- Fast recovery because there is little state to rebuild
- Ideal for auditing and analytics
Cons
- Answering “what is the current value?” needs extra work
- Unbounded data grows forever unless topics are trimmed
- Duplicates in the source appear as real events
- Stream-to-stream joins need windows and extra state
KTable
Pros
- Gives the current answer per key directly
- Storage is proportional to number of keys, not number of events
- Natural fit for lookups and enrichment
- Supports queries against the live state store
- Deletes are clean thanks to tombstones
Cons
- Always stateful, so disk, backups, and restores matter
- History is hidden unless you also keep the stream
- Needs good, non-null keys
- Restart can be slow for very large tables
- Fewer output updates than input records can surprise people
Trade-offs at a glance
| Trade-off | Leans KStream | Leans KTable |
|---|---|---|
| Storage | Grows with number of events | Grows with number of distinct keys |
| Recovery speed | Fast for stateless work | Slower, must rebuild state |
| Answering “current value” | Needs aggregation | Built in |
| Answering “what happened in order” | Built in | Not possible from the table alone |
| Operational effort | Low | Higher, because of state |
| Consistency of view | Every event visible | Eventually consistent, latest wins |
A quiet but important trade-off is consistency. A KTable in your application is a local copy built from Kafka, so it reflects the world with a small delay. Two application instances holding different partitions are each correct for their own keys, but a full-table view across instances is eventually up to date, not instant. For most business uses this is perfectly fine. For strict banking-style checks, teams often combine tables with exactly-once processing and careful design.
You can have both: keep the raw events in a KStream topic for history, and derive a KTable from it for the current state. Storage for the history is cheap, and the table stays small.
Choosing the Right One: A Decision Guide
When you are staring at a new problem, walk through these questions in order.
Does each record stand alone as a fact?
If yes, and losing one would lose information, choose a KStream.
Do you only care about the latest value per key?
If yes, choose a KTable.
Is the data small reference data you need everywhere?
If yes, choose a GlobalKTable.
Do you need to count or sum?
Start with a KStream, group by key, and let the result become a KTable.
Do you need to add context to events?
Use a KStream joined with a KTable or GlobalKTable.
Matching real problems to the right tool
| Business need | Best fit | Why |
|---|---|---|
| Detect fraudulent card payments | KStream | Each payment is an event, and patterns across events matter |
| Show the current profile of a user | KTable | Only the latest profile is relevant |
| Count page views per minute | KStream with window, producing a KTable | Events aggregated into time slices |
| Convert prices using exchange rates | KStream joined with GlobalKTable | Small, shared, slow-changing lookup data |
| Track the live balance of every account | KTable | Balance is current state per account |
| Keep an audit trail of all changes | KStream | History must never be lost |
| Show orders with customer details | KStream joined with KTable | Events enriched by current context |
Worked example: an online store
Orders arrive as a KStream, because each order is a distinct event. Customer profiles live in a KTable, because you want the latest address. A list of tax rates by country sits in a GlobalKTable because it is small and every instance needs it. A single pipeline joins the order with the customer and the tax rate, then counts revenue per minute into a windowed KTable. All three ideas cooperate in one small program.
Security Considerations
Tables quietly keep copies of data in more places than beginners expect.
Protecting the wire and the doors
Encryption
Use TLS between your application and the brokers so records cannot be read on the network.
Authentication
Use mechanisms such as SASL or mutual TLS so only known applications can connect.
Authorization
Use access control lists to say which application may read or write which topic.
Disk protection
Encrypt volumes that hold state stores, since they contain real business data.
Internal topics need permissions too
A Streams application creates its own internal topics for changelogs and repartitioning. The names begin with the application ID. Your access rules must allow the application to create, read, and write topics with that prefix. A common beginner mistake is granting access only to input and output topics, and then wondering why the app fails at startup.
Personal data in tables
A KTable that holds customer profiles stores personal information in three places: the source topic, the local state store, and the changelog topic. If a customer asks to be forgotten, you must remove them from all of these. Sending a tombstone for that key removes the row from the table, and compaction then removes older versions from the topics after a configured delay. Plan this delay and test it, because privacy laws expect deletion to actually happen.
A KStream topic with long retention keeps personal data too. Decide retention with legal and privacy teams in mind. Where possible, store minimal fields, or replace sensitive values with tokens before they enter the pipeline.
Interactive queries and exposure
Kafka Streams lets you query state stores directly from your application, which is handy for showing a live table through an API. Treat that API like any other public service: put it behind an API gateway or authentication layer, and never expose raw state store access to the open internet.
Real-world example: Financial services
Banks that use Kafka apply TLS, strict access control lists, and encrypted disks as a baseline. They also treat changelog topics as sensitive as the original data, because a table rebuilt from a changelog holds exactly the same secrets as the table itself.
Performance and Scalability
Kafka Streams grows by adding helpers, but only if you understand what limits the growth.
Partitions set the ceiling
Remember that one task handles one input partition. If your input topic has 12 partitions, you can have at most 12 tasks working at the same time. Running 20 copies of the application will not make it faster than 12 busy workers. The other eight copies simply wait as spares. So the first scaling decision is really made when the topic is created: how many partitions will I need at my busiest?
A restaurant with 12 tables can only seat 12 groups at once, no matter how many waiters you hire. More waiters help only until every table has one. To serve more groups, you need more tables, and in Kafka, tables are partitions.
Scaling out and scaling up
More instances
Start more copies of the application. Kafka moves some tasks to the new copy automatically.
More threads
Give each instance more stream threads so it can run more tasks at once on a bigger machine.
Faster storage
Tables live on local disk, so fast solid-state drives make lookups and restores much quicker.
Bigger caches
More memory for the record cache and the database block cache means fewer slow disk reads.
What slows a KTable down
A KStream filter is almost free: look at the record, decide, move on. A KTable step is heavier, because each record triggers a read from the state store, a write back, and a queued backup to the changelog. Three things make it heavier still.
- Large rows. Storing big values for every key makes each write slower and the store bigger.
- Very many keys. When the table is too large for memory, lookups go to disk more often.
- Hot keys. One popular key forces one task to do more work than its neighbors.
Throughput and latency
Two words describe speed. Throughput is how many records per second you can process. Latency is how long one record takes from start to finish. They pull against each other. Batching and caching raise throughput but can add a little delay. Tiny commit intervals lower latency but create more small writes. Choose based on what the business needs: a fraud check wants low latency, while a nightly-style analytics count wants high throughput.
Why local state is fast
A big reason Kafka Streams performs well is that table lookups happen on the same machine, on local disk or in memory. There is no network call to a remote database for each record. Compare that with a design where every event triggers a database query over the network: at hundreds of thousands of events per second, the database becomes the bottleneck. Keeping the table right next to the processing code removes that bottleneck.
Real-world example: LinkedIn
LinkedIn, where Kafka was born, runs clusters that handle trillions of messages every day. Its engineers have long argued that keeping processing close to the data, and scaling by partitions, is what lets such volumes stay manageable. The same principle guides Kafka Streams table design at any size.
Before launch, measure how many records per second one task can handle. Divide your expected peak traffic by that number to learn how many partitions you need, then add a comfortable margin for growth.
Log Compaction: The Quiet Engine Behind Tables
Compaction sounds boring, yet without it tables would not survive in Kafka.
Two ways a topic can forget
A normal topic forgets by time or size: records older than, say, seven days are deleted, whatever their keys. A compacted topic forgets by replacement: when a newer record exists for a key, older records for that key may be removed. The topic still keeps the newest record per key, indefinitely.
| Feature | Time-based deletion | Compaction |
|---|---|---|
| What is removed | Old records, regardless of key | Older records of keys that have newer ones |
| Keeps latest per key forever | No | Yes |
| Suits | Event streams, logs, clicks | Tables, profiles, configurations |
| Topic size over time | Steady, bounded by time | Bounded by number of distinct keys |
| Rebuild a table from the topic | Unsafe after retention passes | Safe |
How the cleaner works
Inside each partition, Kafka splits the log into pieces called segments. The newest segment, where records are being written, is left alone. A background worker called the log cleaner looks at older segments, builds a small map of “latest offset for every key”, and writes new, smaller segments that keep only the latest record per key. Old segments are then swapped out.
flowchart LR
A["Before cleaning: Asha 10, Ravi 5, Asha 25, Ravi 0, Asha 40"] --> C["Log cleaner keeps latest per key"]
C --> B["After cleaning: Ravi 0, Asha 40"]
B --> D["Later delete marker removes Ravi if value is empty"]
Before cleaning, the partition holds five records. After cleaning, only the newest record for each key remains. If the newest record for a key is a tombstone, it is kept for a while so that slow readers can learn about the deletion, and then it is removed too.
Imagine a cupboard of old phone-bill copies. Compaction is like keeping only the newest bill from each phone company and recycling the rest. You never lose the current amount you owe, but the cupboard stops overflowing.
Things compaction does not promise
- Not instant. Cleaning runs in the background, so for a while several records per key may still exist.
- Order is kept. Records that survive keep their original offsets and relative order.
- The active segment is untouched. The most recent writes stay uncompacted until the segment closes.
- Readers can still see old records if they read before the cleaner reaches them, which is why table logic must be safe with duplicates for the same key.
A record with a missing key cannot be compacted, because compaction works key by key. Topics that use compaction must reject null keys at the producer.
Real-world example: Consumer offsets
Kafka itself uses compaction. The record of “how far has each consumer group read?” is stored in an internal compacted topic, keyed by group and partition. Only the latest position per key matters, so compaction keeps that topic small no matter how long the cluster runs. It is a table hiding in plain sight.
Deployment and Cloud
A stateful application is easy to write and easy to deploy badly.
Where the application runs
Because Kafka Streams is a library, your application is an ordinary program. It runs in containers, on virtual machines, or on a container platform such as Kubernetes. The brokers can be your own or a managed Kafka service from a cloud provider. The two sides only need a network path and the right permissions.
Stateless is easy, stateful needs care
A pure KStream application keeps nothing important on disk. If a container disappears, another starts and continues from the last committed offset. Rolling updates are simple. A KTable application is different, because its local disk holds a copy of the table. If the disk disappears with the container, the new container must rebuild the table from the changelog before it can work, which can take a long time for a large table.
Persistent disk for state
- Restarts reuse the existing store and catch up only on recent changes
- Rolling upgrades are fast
- Fits large tables
Throwaway disk for state
- Every restart triggers a full rebuild
- Longer pauses during upgrades and failures
- Only sensible for small tables or with standby replicas
Safe rollout steps
Roll one instance at a time
Replace copies gradually so the rest keep serving while tasks move.
Give the app time to shut down cleanly
A clean shutdown commits offsets and flushes state, which makes the next start quick.
Use stable member identity
Static membership lets an instance leave and return briefly without causing a rebalance.
Watch lag and restore progress
Do not roll the next instance until the last one has caught up.
Managing topology changes
If you change the shape of your steps in a way that alters internal topic names or state layout, old state may no longer match. For small changes, careful naming avoids trouble. For large changes, teams run the new version side by side with a new application ID, let it build its own tables from the source topics, and switch consumers over when it is ready. This is safer than editing a running application in place.
Renovating a shop while customers are inside is risky. A safer plan is to build the new shop next door, stock it, and move the sign over once everything works.
Real-world example: Managed Kafka in the cloud
Many companies, including large retailers and banks, run Kafka Streams applications on container platforms while using managed Kafka services for the brokers. The brokers’ durability and upgrades are handled by the provider, while the team focuses on sizing disks for state and setting good alerts on lag.
A Guided Walkthrough: The Online Store
Let us build a pipeline in words, step by step, using every idea from this article.
Our store, “Brightcart”, wants a live view of revenue per city, per minute, with each order showing the customer’s name and the correct tax. Three inputs exist: a topic of orders, a topic of customers, and a small topic of tax rates by country.
flowchart TD
OT["Orders topic"] --> OS["Orders KStream"]
CT["Customers topic compacted"] --> CTB["Customers KTable"]
TT["Tax rates topic compacted"] --> TG["Tax GlobalKTable"]
OS --> J1["Join with customers"]
CTB --> J1
J1 --> J2["Join with tax rates"]
TG --> J2
J2 --> RK["Re-key by city"]
RK --> WA["Windowed count and sum per minute"]
WA --> RT["Revenue KTable per city per minute"]
RT --> OUT["Output topic for dashboards"]
Read orders as a KStream
Each order is an event that happened once. We must not lose any, so a stream is the right view.
Read customers as a KTable
We want the latest name and city per customer. The topic is compacted, so a rebuild is always safe.
Join orders with customers
Both topics use the customer ID as the key and have the same partition count, so the stream-table join works without extra shuffling.
Read tax rates as a GlobalKTable
There are only a couple of hundred countries, so a full copy on every instance is cheap. We can join on the country field, which is not the record key.
Change the key to city
Revenue must be grouped by city, so we re-key. Kafka Streams adds a repartition topic behind the scenes.
Count and sum per minute
A tumbling window of one minute produces a windowed KTable. A small grace period accepts slightly late orders.
Publish only final numbers
Suppression holds results until each minute is closed, so dashboards do not flicker with partial totals.
What could go wrong, and how the design answers it
| Problem | Design answer |
|---|---|
| A customer changes city while an order is in flight | The stream-table join uses the current row, so the order reflects the customer’s latest known city |
| Instance crashes during a busy minute | Tasks move, tables rebuild from changelogs, and exactly-once prevents double counting |
| One large city creates a hot key | Pre-aggregate by city and store before the main step, or split the key across sub-keys and combine later |
| A customer asks to be deleted | A tombstone is sent to the customers topic, and compaction removes older copies later |
| Bad order record with missing fields | It is sent to a dead-letter topic, and the pipeline continues |
Notice how naturally each piece fits its job. Events are streams, current facts are tables, small lookups are global tables, and counting turns a stream into a table again. That is the duality idea at work in a real design.
Myths and Misconceptions
Some wrong ideas keep travelling from team to team. Here is the correction for each.
“A KTable is just a KStream with a different name.”
The difference is semantic and real. A table replaces values by key and stores state. A stream never replaces anything.
“A KTable loses history.”
The table view hides history, but the source topic and changelog still hold it until retention or compaction removes it. Keep the stream if you need the full story.
“More threads always means faster.”
Threads beyond the number of partitions do nothing, and too many threads can add contention on disk.
“State stores are safe without a changelog.”
Without the changelog, a lost disk means lost data. The backup topic is what makes local state reliable.
“A GlobalKTable is always better because it is simpler.”
It is simpler for joins but costs storage on every instance and is unsuitable for large data.
“Exactly-once means my business logic is safe from duplicates everywhere.”
It covers reading, state, and writing inside Kafka. Calls to outside systems, such as sending an email, are not covered and need their own duplicate protection.
That last point trips up many teams. If your processing step calls an external service, make that call safe to repeat, for example by sending a unique request ID so the receiver ignores duplicates.
Interactive Queries: Asking the Table Directly
A KTable is not only a step in a pipeline. It can also answer questions from the outside.
The idea
Normally, a Streams application reads records, processes them, and writes results to an output topic. Other programs then read that topic. But sometimes a website or a mobile app simply wants to ask, “What is the current loyalty balance for Asha?” Writing a whole new database just to answer that can feel heavy. Interactive queries let your application answer straight from its own state store, which is already holding the latest value for every key it owns.
Think of a shop assistant who keeps a small notebook of today’s prices. Customers can walk up and ask, “What does this cost right now?” The assistant answers by looking in the notebook, without phoning head office.
The catch: each instance holds only a slice
A KTable is split across instances by partition. So Asha’s row might live on instance A, while Ravi’s lives on instance C. If a request for Asha arrives at instance C, that instance does not have the answer. Kafka Streams gives you the tools to find out which instance owns a key, and your application then forwards the request to that instance over a normal web call.
flowchart LR
U["Mobile app user"] --> G["API gateway"]
G --> LB["Load balancer"]
LB --> I1["Instance A holds keys for partitions 0 and 1"]
LB --> I2["Instance B holds keys for partitions 2 and 3"]
I1 -- "forwards if key owned elsewhere" --> I2
I1 --> S1[("Store A")]
I2 --> S2[("Store B")]
The user’s request first passes the API gateway, which checks identity, and then the load balancer, which picks any instance. If that instance does not own the key, it forwards the request to the instance that does. The owning instance reads its local store and replies. This hop is quick because it stays inside your own network.
Things to remember
Slightly behind
The store reflects records processed so far. A brand-new event may not appear for a moment.
Restores block reads
While a task rebuilds its state after a move, its keys cannot be answered reliably. Standby replicas can serve older but available answers in some cases.
Shares resources
Queries and stream processing use the same disk and CPU, so heavy query traffic can slow the pipeline.
Guard the door
Serve queries only through an authenticated interface, never straight from the open internet.
Good fit when
- Lookups are simple, by key
- Traffic is modest compared with stream traffic
- You want to avoid running one more database
- Slightly stale answers are acceptable
Poor fit when
- You need complex searches or joins at query time
- Query traffic is heavy or unpredictable
- You need answers guaranteed during restores
- Many different teams need self-service access
Real-world example: Live dashboards
Teams building operational dashboards, such as live delivery status or current order counts per region, often serve small lookups directly from a table for speed. When the dashboards grow popular and need rich filtering, the same teams typically copy the table’s output topic into a dedicated serving database, and keep the streaming application focused on processing.
A useful compromise is to write the table’s updates to an output topic and feed a serving database from that topic. You keep exactly-once processing in the stream, and you give query-heavy users a database designed for them.
Monitoring, Logging, and Metrics
A stream never sleeps, so you need dashboards that watch it while you do.
A batch job either finishes or fails, and you notice. A streaming application can quietly fall behind for hours while still looking “up”. Good monitoring answers three questions: Is it alive? Is it keeping up? Is it healthy inside?
flowchart LR
APP["Streams application metrics"] --> COL["Metrics collector"]
BRK["Broker and topic metrics"] --> COL
LOG["Application logs"] --> LS["Log platform"]
COL --> DASH["Dashboards"]
COL --> AL["Alert rules"]
LS --> DASH
AL --> ONC["On-call engineer"]
Application metrics and broker metrics flow into a collector. Logs flow to a log platform. Dashboards show trends, and alert rules wake up the on-call engineer when a number crosses a line.
The numbers worth watching
| Metric idea | What it tells you | Applies most to |
|---|---|---|
| Consumer lag | How far behind the application is on each input partition | Both |
| Process rate and latency | How many records per second, and how long each takes | Both |
| Commit latency | How long offset commits take, hinting at broker trouble | Both |
| Rebalance count and duration | How often work is shuffled and how long it stalls | Both, but heavier for KTable |
| State store size and read/write latency | Whether the table is growing too fast or the disk is slow | KTable |
| Restore progress | How much of a changelog is left to replay after a restart | KTable |
| Dropped late records | Events arriving after the grace period | Windowed tables |
| End-to-end latency | Time from event creation to result | Both |
The single most useful alert is lag that keeps growing. A small steady lag is fine. Lag that climbs for many minutes means the application cannot keep pace with incoming data.
Logging with care
Log events that help humans understand behavior: rebalances, restores, errors, and dropped records. Do not log full record contents, because that spreads personal data into log files. Use a stable identifier instead, such as an offset and partition, which lets an engineer find the exact record later.
Dead-letter handling
Some records will be broken: unreadable, missing fields, or wrongly formatted. If the application crashes on every bad record, it will restart and crash again forever. A better plan is to catch such records, send them to a separate dead-letter topic, and keep going. A person can inspect that topic later and decide what to fix.
A post office does not stop the whole sorting line because one envelope has no address. It puts that envelope into a special tray for a person to sort out later, while everything else moves on.
Design Patterns and Anti-Patterns
Teams keep discovering the same good habits and the same bad ones.
Patterns that work
Event enrichment
Join a stream of events with a table of current facts so downstream users get complete records.
Materialized view
Aggregate a stream into a table that answers one specific question quickly, such as “orders per customer”.
Event sourcing
Store every change as an event in a stream, and derive current state as a table when needed.
Change data capture
Copy database changes into Kafka as a stream, then rebuild tables from them elsewhere.
Reference data broadcast
Load small lookup data into a GlobalKTable so every instance can join locally.
Deduplication
Use a state store of recently seen IDs to drop repeated events before further processing.
Anti-patterns to avoid
What people do
They read a stream of independent events, such as payments, as a KTable.
Why it hurts
Two payments from the same customer share a key, so the second one silently replaces the first in the table. Money events disappear from the view.
Better approach
Read events as a KStream, and aggregate into a table only when you truly want a running total.
What people do
They use a GlobalKTable for a huge dataset because it avoids co-partitioning rules.
Why it hurts
Every instance stores the entire dataset. Disk bills multiply, and startup becomes painfully slow.
Better approach
Use a regular KTable and fix the partitioning, or use a foreign-key join.
What people do
They build a table on a topic that deletes data after seven days instead of compacting it.
Why it hurts
Keys that never change are removed when they age out, and the table forgets them after a rebuild.
Better approach
Turn on compaction for any topic that represents current state.
What people do
They treat the local state store as a general-purpose database and run heavy queries against it.
Why it hurts
It competes with stream processing for the same disk and CPU, and it only holds a slice of the data per instance.
Better approach
Write results to a proper serving database for heavy queries, and keep interactive queries for light lookups.
What people do
They join two streams with a very wide window, such as a full week, to be “safe”.
Why it hurts
The join must keep a week of records from both sides in state, which balloons storage and slows recovery.
Better approach
Choose the smallest window that matches the real business gap between the two events.
Best Practices and Common Mistakes
Small habits that save big headaches in production.
Habits that pay off
Choose keys on purpose
Pick keys that spread traffic evenly and match how you will group and join later.
Plan partitions early
Partition counts limit how many tasks can run in parallel, and changing them later breaks key placement. Pick a comfortable number from day one.
Prefer changing values over keys
Value-only changes avoid repartition topics. Re-key only when the business question needs it.
Name your steps
Give explicit names to stateful steps. Automatic names depend on step order, so adding a step later can change internal topic names and orphan your saved state.
Use standby replicas for big tables
They cost extra disk but shrink recovery time from minutes to seconds.
Replicate internal topics
Give changelog and repartition topics the same durable replication you give important business topics.
Test with realistic data
Test with late, duplicated, and out-of-order events, and with null keys. Real traffic is messier than demo data.
Common mistakes
Expecting one output per input
A KTable caches and merges updates, so fewer outputs appear than inputs. This is normal.
Forgetting partition matching
Joining topics with different partition counts fails at startup.
Ignoring tombstones
Code that assumes every value is non-empty will break when a deletion record arrives.
Skipping state sizing
Not estimating table size leads to full disks and long restores.
Changing the topology carelessly
Editing steps in a running application can make old state unusable. Use the reset tool and plan migrations.
Using more threads than partitions
Extra threads sit idle, because one task never splits across two threads.
To estimate table size, multiply the number of distinct keys by the average size of a row, then add room for overhead and growth. Doing this on paper before launch prevents most disk surprises.
Real-World and Industry Examples
Seeing the ideas at work in familiar products makes them stick.
Netflix: viewing events as a stream
Play, pause, and stop events are pure streams. Analytics teams process them to spot playback problems and to feed recommendation systems. A separate table view, such as a title’s current catalog details, is joined to those events to explain what each viewer watched.
Uber: locations and trip status
Driver location pings are a stream, since each ping is a fresh fact. The driver’s current status, such as available or on a trip, is a table where only the newest value matters. Matching riders to drivers needs both views working together.
LinkedIn: activity and profiles
Clicks and views flow as streams. Profile fields such as job title behave like table rows. Feeds combine both so you see fresh posts alongside up-to-date names and photos.
The New York Times: one source of truth
Published content lives in Kafka, and each piece of content has a current version. Search, recommendation, and web systems each build their own table view from that same log, and every one of them is rebuilt from scratch whenever needed.
Pinterest: advertising budgets
Ad events form the stream, and each advertiser’s remaining budget forms the table. Joining them lets the system stop showing an ad the moment its budget runs out, avoiding overspending.
Rabobank: customer alerts
Account activity streams in, and a table remembers the current state used to decide whether an alert is worth sending. Fast recovery matters because customers expect alerts within moments.
Frequently Asked Questions
Summary and Key Takeaways
KStream and KTable are two lenses on the same records. A KStream sees every record as a new event, like a diary that only grows. A KTable sees every record as the latest value for its key, like a scoreboard that always shows the present. GlobalKTable is the fully copied cousin, ideal for small lookup data. The two main ideas convert into each other through aggregation and change streams, which is why Kafka Streams feels so consistent once you see the pattern.
Key Takeaways
- Same topic, two meanings: a KStream means insert, and a KTable means upsert.
- Choose by asking one question: is this a fact that happened, or a fact that is currently true?
- KTables are stateful: they use a local state store backed by a changelog topic, so plan for disk, standby replicas, and restore time.
- Keys are destiny: same key means same partition, and changing keys means repartition topics and extra cost.
- Joins have rules: regular joins need co-partitioned inputs, stream-to-stream joins need windows, and stream-to-table joins only fire on the stream side.
- Compaction and tombstones keep tables correct and let you delete data properly, which also matters for privacy.
- Use both together: keep events as streams for history, derive tables for current state, and use a GlobalKTable for small shared reference data.