Skip to main content

Codelab: Produce & Consume with Kafka

Goal: A Real Kafka Log, On Your Laptop

You've read about the log model, offsets, consumer groups, and replay. Now you're going to run them. In the next fifteen minutes you'll stand up a real single-node Kafka broker, create a partitioned topic, produce a handful of order events, and then watch — for real — the exact behaviors the last few lessons described: how a key decides a message's partition and keeps it ordered, how the log keeps everything instead of deleting on read, how two consumer groups read the same stream independently, how you rewind an offset to replay, and how the whole thing survives a hard crash because the log is on disk.

No Docker, no Zookeeper, no cloud account. Modern Kafka (4.x) runs itself in KRaft mode — the broker is its own coordinator — so it's a single process you start from a folder. Everything below was run exactly as written; the output you see is the real output.

This codelab makes concrete: the broker-vs-log model (Queue Models), independent consumers (Pub/Sub), and the delivery mechanics (Delivery Semantics) — the whole of this section, in one terminal.

A 'what you'll build' diagram for a Kafka codelab, showing a single-node Kafka broker running on a laptop with one topic named orders split into three partitions. On the left, a producer sends keyed order events into the topic; a small note reads key hashes to a partition, so all of alice's orders land in partition 0 and stay in order. Each of the three partitions is drawn as an append-only log of numbered cells at offsets zero, one, two, and so on, with new records appended at the end and nothing deleted on read. On the right, two consumer groups read the same topic independently: group fulfillment and group analytics, each with its own offset cursor pointing at a different position along the partitions, labelled each group reads the whole stream at its own offset. A curved rewind arrow loops one group's cursor back to offset zero, labelled replay: reset the offset and re-read. Below the broker, a small disk icon with a lightning bolt and the words survives a crash shows that the log is persisted, so killing and restarting the broker keeps every event. Around the diagram, four terminal command chips name the tools the codelab uses: kafka-storage format, kafka-topics create, kafka-console-producer, and kafka-console-consumer, plus kafka-consumer-groups for offsets and replay. The takeaway line reads: a topic is a partitioned, replayable log — produce once, let many groups read at their own pace. The palette is dark slate with a violet producer, sky-blue partitions, and emerald consumer groups.

Prereqs: Just Java and a Terminal

You need exactly two things: a Java runtime (17 or newer — Kafka runs on the JVM) and a terminal. Check Java, then download and unpack Kafka 4.3.1:

$ java -version
openjdk version "21" 2024-...   # 17+ is fine

# download and unpack Kafka 4.3.1 (~130 MB)
$ curl -sSLO https://downloads.apache.org/kafka/4.3.1/kafka_2.13-4.3.1.tgz
$ tar -xzf kafka_2.13-4.3.1.tgz
$ cd kafka_2.13-4.3.1

That folder is a complete Kafka: the broker, and a bin/ full of command-line tools (kafka-topics.sh, kafka-console-producer.sh, and friends) that talk to it. Everything from here runs inside this directory.

Step 1 — Start a One-Node Kafka (KRaft)

A Kafka broker stores its data in a log directory, and before first use you format that directory with a unique cluster id — the same way you format a disk. Generate an id, format, then start the server:

# generate a random cluster id and format the storage
$ KAFKA_CLUSTER_ID="$(bin/kafka-storage.sh random-uuid)"
$ bin/kafka-storage.sh format -t $KAFKA_CLUSTER_ID -c config/server.properties --standalone
Formatting dynamic metadata voter directory /tmp/kraft-combined-logs with metadata.version 4.3-IV0.

# start the broker (it runs in the foreground; leave this terminal open)
$ bin/kafka-server-start.sh config/server.properties
...
INFO Kafka Server started (kafka.server.KafkaRaftServer)

When you see Kafka Server started, you have a live broker listening on localhost:9092. That one process is both the broker (it stores messages) and the controller (it manages metadata) — that's what KRaft means, and it's why there's no separate Zookeeper to babysit. Open a second terminal in the same folder for everything below; leave this one running the server.

Step 2 — Create a Topic With Partitions

A topic is a named stream. Its parallelism and ordering come from partitions — each partition is one append-only log. Create an orders topic with three partitions, then describe it:

$ bin/kafka-topics.sh --bootstrap-server localhost:9092 \
    --create --topic orders --partitions 3 --replication-factor 1
Created topic orders.

$ bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic orders
Topic: orders   PartitionCount: 3   ReplicationFactor: 1
    Topic: orders   Partition: 0    Leader: 1   Replicas: 1   Isr: 1
    Topic: orders   Partition: 1    Leader: 1   Replicas: 1   Isr: 1
    Topic: orders   Partition: 2    Leader: 1   Replicas: 1   Isr: 1

Three partitions, each with Leader: 1 (our single broker owns all of them) and Replicas: 1 (no copies — this is a one-node toy; in production you'd set --replication-factor 3 so a dead broker loses nothing). Those three partitions are the unit of parallelism you met in the log lesson: at most three consumers in a group can read this topic at once.

Step 3 — Produce Some Events (Keys Choose Partitions)

Now send six order events. We'll give each a key — the customer — because Kafka routes a message to a partition by hashing its key. Same key, same partition, guaranteed order for that customer:

$ printf 'alice:order #1001 placed\nbob:order #1002 placed\nalice:order #1003 placed\ncarol:order #1004 placed\nbob:order #1005 placed\nalice:order #1006 placed\n' | \
  bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic orders \
    --reader-property parse.key=true --reader-property key.separator=:

The producer prints nothing and exits — the six events are now durably on the log. The interesting part is where they went, which the next step reveals.

Step 4 — Consume From the Beginning

Read the topic from the start, printing each message's partition and key:

$ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders \
    --from-beginning --timeout-ms 4000 \
    --formatter-property print.partition=true --formatter-property print.key=true
Partition:0   alice   order #1001 placed
Partition:0   bob     order #1002 placed
Partition:0   alice   order #1003 placed
Partition:0   bob     order #1005 placed
Partition:0   alice   order #1006 placed
Partition:2   carol   order #1004 placed
Processed a total of 6 messages

Two things just happened that the log lesson promised. First, --from-beginning replayed the whole topic — the messages were never consumed away; reading doesn't delete them. Second, look at the partitions: every one of alice's and bob's orders landed in Partition 0, carol's in Partition 2, none in Partition 1 — because partition = hash(key) % 3 is deterministic. Within a partition the order is exact (1001 before 1003 before 1006); across partitions there's no global order. That's the ordering guarantee, and its limit, in one screen. (The timeout-ms just tells this console consumer to quit after 4 seconds of silence instead of waiting forever.)

Step 5 — Consumer Groups: Your Own Offset

So far we read anonymously. Real consumers join a group, and Kafka tracks each group's position — its offset — for every partition. Read the topic as a group called fulfillment, then ask Kafka where that group is:

$ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders \
    --from-beginning --group fulfillment --timeout-ms 4000
order #1001 placed
... (all 6) ...

$ bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group fulfillment
GROUP         TOPIC    PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG
fulfillment   orders   0          5               5               0
fulfillment   orders   1          0               0               0
fulfillment   orders   2          1               1               0

This little table is the whole model on one screen. CURRENT-OFFSET is how far fulfillment has read; LOG-END-OFFSET is how many records exist; LAG is the gap — how far behind the group is. Partition 0 has 5 records and the group has read all 5 (offset 5, lag 0); partition 2 has 1; partition 1 is empty. Now the payoff — start a different group and it reads the entire stream on its own, because its offsets are separate:

$ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders \
    --from-beginning --group analytics --timeout-ms 4000 | wc -l
6

analytics read all 6, even though fulfillment had already "consumed" them. One log, two independent readers, no copies — exactly the fan-out from the pub/sub lesson, and the reason a log can feed your fulfillment pipeline and your data warehouse at the same time without either noticing the other.

Step 6 — Replay: Rewind the Offset

Because a group's position is just a number over data that still exists, you can move it backward. This is replay — the thing a delete-on-ack queue can never do. Rewind fulfillment to the very start and read again:

$ bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
    --group fulfillment --topic orders --reset-offsets --to-earliest --execute
GROUP         TOPIC    PARTITION  NEW-OFFSET
fulfillment   orders   0          0
fulfillment   orders   1          0
fulfillment   orders   2          0

$ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders \
    --group fulfillment --timeout-ms 4000 | wc -l
6

Every partition's offset went back to 0, and re-consuming streamed all six events again — no re-producing, nothing restored from a backup. This is the exact move you'd make to reprocess after a bug: fix the consumer, reset its offset to just before the bad data, and let the retained log replay history through the corrected code. (--to-earliest rewinds to the start; you can also reset --to-datetime yesterday, or --to-offset a specific number.)

Break It — Crash the Broker, Keep the Data

One more promise to check: the log is durable, on disk, not just in memory. Let's prove it the rude way — find the broker and hard-kill it, no graceful shutdown:

# in a third terminal — SIGKILL the broker (a hard crash)
$ kill -9 $(jps -l | grep kafka.Kafka | awk '{print $1}')

# restart it
$ bin/kafka-server-start.sh config/server.properties
...
INFO Kafka Server started (kafka.server.KafkaRaftServer)

# the topic and every event are still there
$ bin/kafka-topics.sh --bootstrap-server localhost:9092 --list
__consumer_offsets
orders
$ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders \
    --from-beginning --timeout-ms 4000 | wc -l
6

The broker died mid-life and came back with the topic and all six events intact — they were written to the log directory on disk, so a crash costs nothing. (You also just spotted __consumer_offsets in the list: that's the internal topic where Kafka itself stores every group's offsets — the log is even used to remember where everyone is reading.) On a single node a lost disk would still lose data, which is exactly why production runs replication factor 3 across brokers — but the log itself is durable, and you just watched it survive.

Clean Up

When you're done, stop the broker and remove its data directory:

# stop the broker gracefully (or just Ctrl-C the server terminal)
$ bin/kafka-server-stop.sh

# remove the log/data directory to reset to a clean slate
$ rm -rf /tmp/kraft-combined-logs

What Just Happened: Every Command Was a Concept

You didn't just run commands — you performed the entire section. Map it back:

  • --create --partitions 3 — a topic is a named log split into partitions; partitions are the unit of parallelism and the boundary of ordering. (Queue Models)
  • keyed produce → Partition: 0 for alicehash(key) % partitions places a message, so a key pins all its events to one partition and keeps them ordered. (Queue Models)
  • --from-beginning re-reads everything — a log retains records; consuming doesn't delete them. (Queue Models)
  • --describe offsets and lag — each group tracks its own position; lag is how far behind it is. This is the number you alarm on in production. (Message Queues)
  • a second group reads all 6 independently — many groups fan out over one log, no copies. (Pub/Sub)
  • --reset-offsets --to-earliest then re-read — replay is just moving your cursor back over retained data; it's how you reprocess after a bug. (Queue Models / Delivery Semantics)
  • kill -9 then restart, data intact — the log is durable on disk; a crash is survivable, which is what makes at-least-once delivery possible in the first place. (Delivery Semantics)

Takeaways

  • Modern Kafka is one process. KRaft mode makes the broker its own controller — no Zookeeper, no Docker, just kafka-storage format then kafka-server-start.
  • A topic is a partitioned, replayable log. Partitions give parallelism and per-key ordering; offsets give each consumer group its own independent position; retention lets you rewind and replay.
  • The CLI is the real thing. kafka-topics, kafka-console-producer/consumer, and kafka-consumer-groups are the same tools you'll use to inspect a production cluster — reading offsets and lag with --describe and replaying with --reset-offsets are day-job skills, not toys.
  • Everything you read in this section is observable in a terminal. Broker vs log, fan-out to groups, replay, durability — you can always drop down to the CLI and watch the guarantee instead of trusting it.

That's the end of Real-time & Async Communication. Next comes the section checkpoint: a few product features where you'll choose the communication style — poll, stream, webhook, queue, log, or pub/sub — and the delivery guarantee, and defend the call.