In this lab, you will produce a fixed set of order records into a three-partition topic and read them back, printing each record’s partition, offset, and timestamp. That evidence shows where keyed and unkeyed records land, what a partition's offsets look like, which clock produced each timestamp, and what happens to key placement when the partition count grows. You will finish by resetting a consumer group's committed offset and watching the replay.
Use this fixed input format so you can check the results. Every record value carries a seq field numbered in production order, so you can compare production order to read order. Every keyed record uses one of four order ids. The console consumer always runs with print.key, print.partition, print.offset, and print.timestamp, so no observation is missing its location.
CreateTime timestamp from a LogAppendTime timestamp using stored records.Environment E1: a single combined broker and KRaft controller in the container kafka-local, image apache/kafka:4.3.1. Every Kafka tool runs inside the container through docker exec. The host provides Docker, a Bash-compatible shell, and the record files; it needs no Kafka installation.
Create this tree on the host and run every command from kafka-labs/keys-partitions-and-ordering/:
Start the broker:
Save this as records/keyed-orders.txt. The console producer splits each line at the first |: the key is the text before it and the JSON is the value. The four order ids rotate, giving three records per key while seq counts production order across all twelve:
Save records/unkeyed-orders.txt with the same twelve JSON values, one per line, with no key or separator. Save records/status-updates.txt with two lines:
Every consumer run uses --from-beginning and --timeout-ms 10000, so the tool prints the retained records and exits once nothing arrives for ten seconds. Leave any summary line it appends to an observation file in place.
Create the topic with three partitions and confirm the layout:
Produce the twelve keyed records. The -i flag passes the host file to the tool's standard input:
Read everything back with location metadata and save the output:
Each line has this shape, with tab separators:
Then ask the broker for the end offset of each partition:
Start partition-map.md with one row per key and the partition it landed on. Add the module's prediction: the keyed route takes murmur2(keyBytes) & 0x7fffffff modulo the partition count, which for the UTF-8 bytes of ord-1042 and three partitions is 0.
What you should see. All three records for a key carry the same Partition: value, and their seq values increase down the file. Within one partition, offsets run 0, 1, 2, ... with no gap, and the last offset plus one equals that partition's end offset. The three end offsets sum to 12. One partition may hold nothing; with four keys and three partitions, hashing does not promise to fill every partition, and the report should say why an empty partition is not an error.
Create a second topic and produce the unkeyed file with default producer settings:
Read it back with the Phase 1 consumer command, changing the topic to orders.events and the output file to observations/phase2-unkeyed-default.txt. Count the distinct partitions. Save the end offsets before publishing the second run:
The Java producer's built-in unkeyed routing is sticky: it collects records for a partition to help form batches. Accumulated bytes and batch readiness affect when it selects another partition. Reduce batch.size to encourage more frequent selections, then publish the same file again. This does not promise a different partition after every record:
Read the topic again into observations/phase2-unkeyed-spread.txt. The file contains both runs; identify second-run records using the end offsets you saved after the first run.
What you should see. Every key prints as null. Record how many partitions each run used, using the saved end offsets to separate the runs. A short run can concentrate on one partition; reducing the batch size may spread it more widely, but no specific distribution is required. Within each partition, seq increases with offset in each run. Across partitions, the consumer may print a different seq order, and another read may interleave the partitions differently. If the output happens to match production order, explain why that observation does not establish an ordering guarantee.
Create a status topic with the default timestamp type and produce the two status records:
The console producer cannot set a timestamp, so the producer's clock supplies CreateTime at send time. Switch the topic to broker-assigned timestamps:
Confirm the override with the same tool and --describe. Note the host clock with date +%s, produce records/status-updates.txt a second time, and read the topic from the beginning into observations/phase3-timestamps.txt.
What you should see. Four records at offsets 0 through 3 on partition 0. The first two begin with CreateTime: and the last two with LogAppendTime:; the config change applied to later appends and did not rewrite stored records. Record all four millisecond values and the host clock reading, and compute the gap between the last CreateTime and the first LogAppendTime. Explain which clock produced each number, and what the four values would show if the producer had been a remote machine with a clock several minutes behind the broker.
Increase orders.placed from three partitions to five:
Describe the topic to confirm PartitionCount: 5, produce records/keyed-orders.txt a second time with the Phase 1 producer command, and read the topic from the beginning into observations/phase4-after-alter.txt.
Complete partition-map.md with a second column: the partition each key landed on after the change. Add the module's prediction for ord-1042, whose remainder with five partitions is 3.
What you should see. The first twelve records are still where Phase 1 left them; expansion moves nothing. The new records for at least one key sit on a different partition than that key's earlier records, and partitions 3 and 4 start at offset 0. A moved key's history now spans two partitions with no offset relationship between them.
In the report, compare one ord-1042 record from Phase 1 with its second publication in Phase 4. Identify each by topic, partition, and offset: seq and eventId repeat because you published the same input file twice. Which consumer would own each partition, and what prevents the new partition's consumer from acting before the old partition's earlier work finishes? Also state where a producer that had not yet refreshed its metadata would send the same key.
Read orders.placed through a named group so the group saves a checkpoint on exit:
Describe the group once the consumer has exited:
Copy the CURRENT-OFFSET, LOG-END-OFFSET, and LAG columns for all five partitions into observations/phase5-offset-reset.txt. Then preview and apply a reset of partition 0 to offset 1. A reset needs a group with no active members, which is why the consumer had to exit first:
Describe the group again, then run the group consumer command a second time, unchanged, and append its output to the observation file.
What you should see. After the first read, every partition's CURRENT-OFFSET equals its LOG-END-OFFSET and LAG is 0. After the reset, partition 0 shows CURRENT-OFFSET 1 and a lag of its end offset minus one, while the other partitions are unchanged. The second read prints partition 0 only, from Offset:1 to its last offset. The --from-beginning flag was present both times, so explain why the second read did not start at offset 0, and why the replayed records carry the same offsets and timestamps as before.
Size a production version of orders.placed using the module's method and these hypothetical load-test values:
Show the write-side estimate, each group's consumer estimate, the backlog and required processing rate for recovery, and the count you would test first. State which requirement set the bound and whether assignment granularity leaves any consumer above its design rate.
What you should see. The recovery requirement, not the steady-state estimate, sets the bound. Any candidate above it needs a reason from the module's cost discussion, and the report must connect the choice to what Phase 4 showed about raising the count later.
records/: the three input files.observations/: the six consumer output files named in the project layout and phase2-first-end-offsets.txt, unedited.partition-map.md: one row per key with the partition at three partitions, the partition at five, and whether the key moved.report.md answering these named questions:seq order? Explain why either result is compatible with per-partition ordering.--from-beginning on the second group read, and what the reset did and did not change.observations/phase1-keyed.txt contains twelve records, each key appears with exactly one partition number, and per-partition offsets form a contiguous range from 0.orders.placed after Phase 1 sum to 12.observations/phase2-unkeyed-spread.txt shows null keys and documents the observed second-run partition distribution.observations/phase3-timestamps.txt has two CreateTime: lines followed by two LogAppendTime: lines on partition 0, offsets 0 to 3.partition-map.md has four rows, at least one marked as moved, and ord-1042 reads 0 before and 3 after.observations/phase5-offset-reset.txt shows partition 0 at CURRENT-OFFSET 1 after the reset and a second read that starts at Offset:1 on partition 0 only.5,400,000 records and a required rate of 9,000 records per second.order-reporting group, and no kafka-local container.retention.ms on orders.events with kafka-configs.sh, wait for cleanup, and show with the offsets tool that the log start offset advanced without renumbering the surviving records.order-reporting at the same time against the five-partition topic and record which partitions each one prints.Delete the group and the three topics:
Use --list on both tools to verify that the three lab topics and order-reporting are gone. Kafka’s internal __consumer_offsets topic may remain until you remove the container. Stop and remove the broker, then confirm with docker ps -a that no kafka-local container remains:
Keep the observation files and report.
The recorded partitions and offsets turn the module's claims into facts about one broker. With unchanged key bytes and an unchanged partition count, every record for a key reached one partition, and that partition's offsets counted up without a gap. Unkeyed records followed the sticky path instead, and reading across partitions showed that offsets constrain order within each partition, not between partitions. The timestamp prefix told you which clock the broker stored, and changing the topic setting affected only later appends.
Two phases showed where the guarantees stop. Adding partitions changed the remainder of the same hash, so a key's history split across two logs while nothing moved, which is why expansion needs coordination for consumers that depend on per-key sequence. Resetting a committed offset changed only the group's restart position; the records, offsets, and timestamps stayed the same. Those boundaries are what a partition count has to respect when you size a topic for its workload and recovery target.