KStream vs KTable in Kafka

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.

i
Quick answer

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.

Everyday analogy

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”.

Model one

KStream

An endless diary of events. Every record is a new fact that happened. Nothing replaces anything.

Model two

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"]
    
FIG 1 — The same five records, read two different ways

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.

Insert
KStream meaning of a record
Upsert
KTable meaning of a record
1 Topic
Can feed both views
Everyday analogy

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.

QuestionKStreamKTable
What does one record mean?A new eventThe newest value for a key
Are older records still visible?Yes, all of themNo, they are replaced
Can the same key appear many times?Yes, freelyOnce, as the latest row
Does it need to remember things?Not usually (stateless)Yes, it keeps a state store
Typical exampleClicks, payments, sensor readingsUser 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.

Everyday analogy

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.

Stateless

Filter

Keep only the records you care about, such as payments above a certain amount.

Stateless

Map values

Change the content of each record, such as converting currency, without touching the key.

Stateless

Branch or split

Send records to different paths, such as domestic orders and international orders.

Stateless

Flat map

Turn one record into several, such as splitting one order into its line items.

Stateful

Aggregate

Count, sum, or average records grouped by key. The result is a KTable.

Stateful

Join

Combine events with other streams or tables to add context.

!
Watch out

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.

Everyday analogy

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.

i
Good to know

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.

Everyday analogy

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"]
    
FIG 2 — Stream and table can be converted back and forth

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.

Everyday analogy

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.

A table is a stream at rest. A stream is a table in motion.

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
    
FIG 3 — Components of one Kafka Streams application instance

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

Unit of work

Task

Kafka Streams creates one task per input partition. A task owns its slice of data and its own state store slice.

Worker

Stream thread

A thread runs one or more tasks. You choose how many threads each instance runs.

Process

Application instance

One running copy of your program. Add more copies to scale out.

Memory

State store

The local database that holds table rows and running aggregates.

Backup

Changelog topic

A compacted Kafka topic that copies every state store change.

Reshuffle

Repartition topic

An internal topic used when the key changes, so same-key records meet again.

Everyday analogy

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.

PartUsed by KStream?Used by KTable?
Source nodeYesYes
State storeOnly for stateful stepsAlways
Changelog topicOnly for stateful stepsYes, or reuses the source topic
Repartition topicWhen the key changesWhen grouping by a new key
Sink nodeYesYes, 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.

1

The record lands in a partition

A producer writes the record. Kafka picks the partition using the key, and the record gets its offset.

2

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.

3

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.

4

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.

5

The change is backed up

The state change is queued for the changelog topic, so another machine could rebuild it after a crash.

6

Results move forward

The result goes to the next processor or to a sink node that writes to an output topic.

7

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.

i
Tip

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.

Everyday analogy

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
    
FIG 4 — Recovery from a crashed instance using the changelog and a standby

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 failsWhat happensHow to soften it
One application instance diesIts tasks move to other instances, which rebuild state from changelogsEnable standby replicas
Local disk is wipedFull restore from the changelog on restartUse persistent volumes for state directories
A Kafka broker diesReplicas on other brokers take over, and the app keeps runningUse replication factor of three
Very large table restoreLong pause before processing resumesStandbys, smaller tables, faster disks
Source topic data expiredA KTable built from it can lose rowsUse compaction on table source topics
!
Watch out

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"]
    
FIG 5 — Enriching a stream of orders with a table of customers

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 typeMeaningTriggers output when
KStream to KStreamMatch two event streams that happen close together in timeEither side arrives inside the time window
KStream to KTableAdd current context to each eventOnly when the stream side arrives
KStream to GlobalKTableSame, but using any field and full local copyOnly when the stream side arrives
KTable to KTableKeep a joined table up to dateEither table changes
KTable to KTable by foreign keyJoin on a field other than the keyEither 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.

Everyday analogy

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.

!
Watch out

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.

Everyday analogy

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

Fixed

Tumbling

Back-to-back slices that never overlap, like every hour on the hour. Each event belongs to exactly one window.

Overlapping

Hopping

Fixed-length slices that start more often than they end, such as a ten-minute window that moves every two minutes.

Continuous

Sliding

Windows defined by how close events are to each other, useful for questions like “any two logins within a minute”.

Activity-based

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.

i
Tip

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.

1

Records arrive keyed by order

Each order sits in a partition chosen by its order ID.

2

Your code selects a new key

You choose the customer ID as the key for counting.

3

Repartition topic is used

Records are written out and read back so same-customer records meet in one partition.

4

The count is updated

The stateful step reads the customer’s row, adds one, and stores it.

5

The KTable row is ready

Each customer now has one row showing their current order count.

!
Watch out

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.

Everyday analogy

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-offLeans KStreamLeans KTable
StorageGrows with number of eventsGrows with number of distinct keys
Recovery speedFast for stateless workSlower, must rebuild state
Answering “current value”Needs aggregationBuilt in
Answering “what happened in order”Built inNot possible from the table alone
Operational effortLowHigher, because of state
Consistency of viewEvery event visibleEventually 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.

i
Tip

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.

1

Does each record stand alone as a fact?

If yes, and losing one would lose information, choose a KStream.

2

Do you only care about the latest value per key?

If yes, choose a KTable.

3

Is the data small reference data you need everywhere?

If yes, choose a GlobalKTable.

4

Do you need to count or sum?

Start with a KStream, group by key, and let the result become a KTable.

5

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 needBest fitWhy
Detect fraudulent card paymentsKStreamEach payment is an event, and patterns across events matter
Show the current profile of a userKTableOnly the latest profile is relevant
Count page views per minuteKStream with window, producing a KTableEvents aggregated into time slices
Convert prices using exchange ratesKStream joined with GlobalKTableSmall, shared, slow-changing lookup data
Track the live balance of every accountKTableBalance is current state per account
Keep an audit trail of all changesKStreamHistory must never be lost
Show orders with customer detailsKStream joined with KTableEvents 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.

Ask “is this a fact that happened, or a fact that is currently true?” and the answer picks the tool.

Security Considerations

Tables quietly keep copies of data in more places than beginners expect.

Protecting the wire and the doors

In transit

Encryption

Use TLS between your application and the brokers so records cannot be read on the network.

Identity

Authentication

Use mechanisms such as SASL or mutual TLS so only known applications can connect.

Permission

Authorization

Use access control lists to say which application may read or write which topic.

At rest

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.

!
Watch out

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?

Everyday analogy

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

Scale out

More instances

Start more copies of the application. Kafka moves some tasks to the new copy automatically.

Scale up

More threads

Give each instance more stream threads so it can run more tasks at once on a bigger machine.

Disk

Faster storage

Tables live on local disk, so fast solid-state drives make lookups and restores much quicker.

Memory

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.

1 : 1
Task to input partition
Local
State reads never cross the network
Linear
Scaling until tasks equal partitions

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.

i
Tip

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.

FeatureTime-based deletionCompaction
What is removedOld records, regardless of keyOlder records of keys that have newer ones
Keeps latest per key foreverNoYes
SuitsEvent streams, logs, clicksTables, profiles, configurations
Topic size over timeSteady, bounded by timeBounded by number of distinct keys
Rebuild a table from the topicUnsafe after retention passesSafe

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"]
    
FIG 6 — Compaction keeps the newest record for every key

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.

Everyday analogy

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.
!
Watch out

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

1

Roll one instance at a time

Replace copies gradually so the rest keep serving while tasks move.

2

Give the app time to shut down cleanly

A clean shutdown commits offsets and flushes state, which makes the next start quick.

3

Use stable member identity

Static membership lets an instance leave and return briefly without causing a rebalance.

4

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.

Everyday analogy

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"]
    
FIG 7 — Brightcart revenue pipeline using stream, table, and global table
1

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.

2

Read customers as a KTable

We want the latest name and city per customer. The topic is compacted, so a rebuild is always safe.

3

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.

4

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.

5

Change the key to city

Revenue must be grouped by city, so we re-key. Kafka Streams adds a repartition topic behind the scenes.

6

Count and sum per minute

A tumbling window of one minute produces a windowed KTable. A small grace period accepts slightly late orders.

7

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

ProblemDesign answer
A customer changes city while an order is in flightThe stream-table join uses the current row, so the order reflects the customer’s latest known city
Instance crashes during a busy minuteTasks move, tables rebuild from changelogs, and exactly-once prevents double counting
One large city creates a hot keyPre-aggregate by city and store before the main step, or split the key across sub-keys and combine later
A customer asks to be deletedA tombstone is sent to the customers topic, and compaction removes older copies later
Bad order record with missing fieldsIt 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.

Myth

“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.

Myth

“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.

Myth

“More threads always means faster.”

Threads beyond the number of partitions do nothing, and too many threads can add contention on disk.

Myth

“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.

Myth

“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.

Myth

“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.

i
Good to know

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.

Everyday analogy

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")]
    
FIG 8 — A request may need to hop to the instance that owns the key

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

Freshness

Slightly behind

The store reflects records processed so far. A brand-new event may not appear for a moment.

Availability

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.

Load

Shares resources

Queries and stream processing use the same disk and CPU, so heavy query traffic can slow the pipeline.

Safety

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.

i
Tip

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"]
    
FIG 9 — A simple monitoring pipeline for a Streams application

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 ideaWhat it tells youApplies most to
Consumer lagHow far behind the application is on each input partitionBoth
Process rate and latencyHow many records per second, and how long each takesBoth
Commit latencyHow long offset commits take, hinting at broker troubleBoth
Rebalance count and durationHow often work is shuffled and how long it stallsBoth, but heavier for KTable
State store size and read/write latencyWhether the table is growing too fast or the disk is slowKTable
Restore progressHow much of a changelog is left to replay after a restartKTable
Dropped late recordsEvents arriving after the grace periodWindowed tables
End-to-end latencyTime from event creation to resultBoth
i
Tip

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.

Everyday analogy

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

Pattern

Event enrichment

Join a stream of events with a table of current facts so downstream users get complete records.

Pattern

Materialized view

Aggregate a stream into a table that answers one specific question quickly, such as “orders per customer”.

Pattern

Event sourcing

Store every change as an event in a stream, and derive current state as a table when needed.

Pattern

Change data capture

Copy database changes into Kafka as a stream, then rebuild tables from them elsewhere.

Pattern

Reference data broadcast

Load small lookup data into a GlobalKTable so every instance can join locally.

Pattern

Deduplication

Use a state store of recently seen IDs to drop repeated events before further processing.

Anti-patterns to avoid

ANTI-PATTERN 01Avoid
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.

ANTI-PATTERN 02Avoid
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.

ANTI-PATTERN 03Avoid
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.

ANTI-PATTERN 04Avoid
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.

ANTI-PATTERN 05Avoid
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

1

Choose keys on purpose

Pick keys that spread traffic evenly and match how you will group and join later.

2

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.

3

Prefer changing values over keys

Value-only changes avoid repartition topics. Re-key only when the business question needs it.

4

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.

5

Use standby replicas for big tables

They cost extra disk but shrink recovery time from minutes to seconds.

6

Replicate internal topics

Give changelog and repartition topics the same durable replication you give important business topics.

7

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

Mistake

Expecting one output per input

A KTable caches and merges updates, so fewer outputs appear than inputs. This is normal.

Mistake

Forgetting partition matching

Joining topics with different partition counts fails at startup.

Mistake

Ignoring tombstones

Code that assumes every value is non-empty will break when a deletion record arrives.

Mistake

Skipping state sizing

Not estimating table size leads to full disks and long restores.

Mistake

Changing the topology carelessly

Editing steps in a running application can make old state unusable. Use the reset tool and plan migrations.

Mistake

Using more threads than partitions

Extra threads sit idle, because one task never splits across two threads.

i
Tip

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.

Nearly every real system uses both: events for what happened, and tables for what is true now.

Frequently Asked Questions

Q1Is a KTable a real database table?
No. It behaves like a table, but it is built from a topic and stored in a local state store, with a changelog topic as backup. It is a live copy of state, not a general-purpose database.
Q2Can the same topic be both a KStream and a KTable?
Yes. Nothing about the topic changes. You choose the reading style in your code, and you can read one topic both ways in different parts of the same application, though this doubles the effort for the table side.
Q3What happens to a KTable if I send a record with an empty value?
That record is a tombstone. The row for that key is removed from the table.
Q4Why does my KTable produce fewer outputs than inputs?
Because of the record cache. Rapid updates to the same key are merged, and only the latest is sent when the cache flushes. Reducing the cache size or the commit interval gives more frequent output.
Q5When should I pick a GlobalKTable over a KTable?
Choose a GlobalKTable for small, slow-changing reference data that many instances must look up, especially when you need to join on a field that is not the key. Choose a KTable for large or fast-changing data.
Q6Do I need windows for KStream to KTable joins?
No. Windows are needed for stream-to-stream joins, because both sides are endless. A stream-to-table join simply looks up the table’s current row for each arriving event.
Q7Why does a KStream to KTable join produce nothing when the table changes?
Only the stream side triggers output in that join. Table updates change what future events will see, but they do not create new output by themselves. If you need updates from both sides, join two tables instead.
Q8What is a tombstone and why does compaction matter for it?
A tombstone is a record with a key and an empty value, meaning “delete this key”. Compaction keeps the latest record per key, so a tombstone eventually removes older values, and later the tombstone itself is cleaned away after a configured delay.
Q9How many application instances can I run?
Work is divided into tasks, one per input partition. You can run instances and threads until each task has its own thread. Beyond that, extra threads or instances sit idle and only act as spare capacity for failures.
Q10Is exactly-once processing always needed for tables?
Not always. For counters where a tiny overcount is harmless, at-least-once is fine and faster. For balances, inventory, and billing, exactly-once is strongly recommended.
Q11Can I look up a KTable value from outside the application?
Yes, through interactive queries. Each instance holds only part of the table, so a request may need to be routed to the instance that owns the key. Protect this endpoint like any other service.
Q12What if two topics I want to join have different partition counts?
Regular joins will fail. Options include repartitioning one topic to match, using a GlobalKTable for the smaller one, or using a foreign-key join between tables.
Q13Can I change the number of partitions after the application is running?
You can add partitions to a topic, but doing so changes which partition a given key maps to. Existing table rows and co-partitioned joins then no longer line up, so most teams create a new topic with the right count and migrate to it instead.
Q14Which is cheaper to run, a KStream or a KTable application?
A stateless KStream application is usually cheaper, because it needs no large disks, no standby replicas, and no long restores. A KTable application costs more in storage and in operational care, but that cost buys you the current answer per key without extra systems.
Q15What happens to a KTable when late or out-of-order records arrive?
A plain table simply applies records in the order it reads them from the partition, so the latest record read becomes the current value. If timestamp-correct behavior matters, use versioned state stores or design keys and windows so late data is handled explicitly.
Q16How do I test a topology without a real cluster?
Kafka Streams ships with a test driver that runs your steps in memory. You feed it sample records, including tombstones, late events, and null keys, and check the output. It is fast and lets you test the tricky table behaviors long before production.

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.