Interview board / backend infrastructure / v1

Kafka, by the shape of the log

Kafka is one idea repeated everywhere: an append-only file that readers walk at their own pace. Every command, guarantee, and failure mode on this board falls out of that single fact. Commands are tagged by which side of the system they touch: produce, consume, or administer.

← all topics
produce, writes to the log consume, reads the log admin, transactional, or cluster level trap, say it out loud before the interviewer does

01  One topic, five views

the whole system in one picture

A topic is a named stream of records, split into partitions, and each partition is an append-only file on disk. Nothing below is a separate feature bolted on. Each card is the same topic, orders, viewed from a different angle: how it is stored, how a record picks a partition, how readers divide the work, how it survives a dead machine, and how it is eventually reclaimed.

Read these five cards as one topic seen five ways, not as five topics A single orders topic created with six partitions and replication factor three is simultaneously all five pictures below. It is six append-only logs (card one), each of which receives records whose key hashes to it (card two), which are divided among the members of every consumer group independently (card three), and each of which exists as three copies on three brokers with one of them elected leader (card four), and each of which discards old segments on whatever policy you set (card five). One create command, quoted in full on card one, produces all of that.

The partition logAppend-only file, records addressed by offset

orders  /  partition 0append-only log
placedoffset 0
paidoffset 1
packedoffset 2
shippedoffset 3
deliveredoffset 4

An offset is a position in one partition, assigned by the broker on append and never reused. It is not a global sequence number and it means nothing in another partition. Records are immutable once written: there is no update and no delete by key, only appending and eventually reclaiming from the tail.

Create it, write to it, read it

# create the topic: 6 partitions, 3 copies of each
kafka-topics.sh --bootstrap-server localhost:9092 \
  --create --topic orders --partitions 6 --replication-factor 3

# append records
kafka-console-producer.sh --bootstrap-server localhost:9092 \
  --topic orders

# read the whole log from offset 0, then keep following it
kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic orders --from-beginning

# read one partition from one exact offset
kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic orders --partition 0 --offset 3
Reading does not consumeReading a record leaves it in place. Ten consumer groups can each read offset 3 of partition 0, at different times, without affecting each other. This is the property that separates Kafka from a traditional message queue, where a delivered message is removed.

Key to partitionThe key decides placement, and therefore ordering

A record carries an optional key. If the key is present, the producer hashes it and takes the remainder against the partition count, so the same key always lands on the same partition. If the key is null, the producer spreads records across partitions in batches instead.

orders  /  6 partitionsmurmur2(key) & 0x7fffffff % 6
key user-42murmur2 = 1459644460partition 4
key user-77murmur2 = -632950913partition 3
key user-108murmur2 = -1644591886partition 4
key order-9001murmur2 = 553625079partition 3
key cart-42murmur2 = 1304770459partition 1

Those five results are the real output of Kafka's default partitioner, which is the murmur2 hash with the sign bit cleared. Note that user-42 and user-108 both land on partition 4, and that cart-42 does not land with user-42: partitions are shared by unrelated keys, and only exact key equality guarantees co-location.

Produce with keys, then inspect the layout

# produce keyed records, tab separated
kafka-console-producer.sh --bootstrap-server localhost:9092 \
  --topic orders --property parse.key=true --property key.separator=$'\t'

# show the partition list and where each leader lives
kafka-topics.sh --bootstrap-server localhost:9092 \
  --describe --topic orders

# read back with the key and partition printed, to verify placement
kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic orders --from-beginning \
  --property print.key=true --property print.partition=true
Ordering exists only inside one partitionKafka guarantees that records in a partition are delivered in the order they were appended. It guarantees nothing about the relative order of records in different partitions. So ordering is something you buy by choosing a key: key by order-9001 and every event for that order is ordered, while events for different orders are not ordered against each other, which is almost always what you actually want.

The consumer groupPartitions divided among members, offsets committed per group

A consumer group is a set of processes sharing one subscription. The group coordinator assigns each partition to exactly one member, so adding members increases parallelism only up to the partition count. A sixth partition and a seventh consumer means the seventh consumer sits idle.

group billing  /  6 partitions, 3 membersassignment
consumer Apartitions 0, 1committed offset 4812
consumer Bpartitions 2, 3committed offset 3390
consumer Cpartitions 4, 5committed offset 5107

The committed offset is stored per group, per partition, in an internal topic named __consumer_offsets. That is why a second group reading the same topic is completely independent: it has its own row in that topic and its own position. It is also why lag is defined per group, as the difference between the end of the partition and the group's committed offset.

Join a group, then measure it

# two shells running this line form a two-member group
kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic orders --group billing

# the single most useful operational command: per-partition lag
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --describe --group billing

# rewind the whole group to the start, group must be stopped
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --group billing --topic orders \
  --reset-offsets --to-earliest --execute
Partition count is your parallelism ceilingYou can add consumers freely, but a group can never have more useful members than the topic has partitions. This is the reason topics are usually created with more partitions than currently needed, and the reason the follow-up question is always what happens when you need more later. Card four in cluster 04 answers that.

ReplicationEvery partition exists N times, one leader answers

Replication factor three means each partition is stored on three brokers. One replica is elected leader and serves all reads and writes for that partition; the other two are followers that continuously fetch from the leader. A follower that is caught up is in the in-sync replica set, written ISR.

orders  /  partition 0  /  replication factor 3leader and followers
broker 1leader, accepts produce and fetchin sync
broker 2follower, fetching from broker 1in sync
broker 3follower, fell behind by 9sout of sync

With that state, the ISR holds two replicas. If the topic is configured to require two in-sync replicas and the producer asks for full acknowledgement, writes still succeed. If broker 2 also falls out, the ISR drops to one, and those writes now fail rather than silently accepting data that only exists on a single machine.

Set the durability floor and read the current state

# require at least 2 of the 3 replicas to hold the write
kafka-configs.sh --bootstrap-server localhost:9092 \
  --alter --entity-type topics --entity-name orders \
  --add-config min.insync.replicas=2

# producer side, the other half of the same guarantee
acks=all

# shows Leader, Replicas, and Isr per partition
kafka-topics.sh --bootstrap-server localhost:9092 \
  --describe --topic orders

# list partitions whose leader is not the preferred one
kafka-topics.sh --bootstrap-server localhost:9092 \
  --describe --topic orders --under-replicated-partitions
The two settings only work as a pairacks=all alone means "all replicas currently in sync", which is one replica if the other two have fallen out, so it is not durable by itself. min.insync.replicas=2 alone does nothing if the producer does not wait. Quoting both together is the answer the interviewer is listening for.

Retention and compactionTwo ways a log is reclaimed

A log cannot grow forever, and the policy you choose changes what the topic fundamentally is. Time or size retention makes the topic a window of recent history. Compaction makes it a table: it keeps the most recent record for every key and discards earlier records for that key.

accounts  /  cleanup.policy=compactbefore compaction
offset 0key user-42, balance 100superseded
offset 1key user-77, balance 50superseded
offset 2key user-42, balance 140superseded
offset 3key user-77, balance 65latest for key
offset 4key user-42, balance 210latest for key
accounts  /  after compactionoffsets are preserved, not renumbered
offset 3key user-77, balance 65kept
offset 4key user-42, balance 210kept

Offsets 0, 1, and 2 are gone and their offset numbers are never reassigned, so a compacted log has gaps. A consumer reading from the beginning now receives one record per key, which is exactly the state of the accounts table. Writing a record with a key and a null value, called a tombstone, deletes that key from the compacted result.

Set either policy

# window of history: keep 7 days
kafka-configs.sh --bootstrap-server localhost:9092 \
  --alter --entity-type topics --entity-name orders \
  --add-config retention.ms=604800000

# table of latest state per key
kafka-topics.sh --bootstrap-server localhost:9092 \
  --create --topic accounts --partitions 6 --replication-factor 3 \
  --config cleanup.policy=compact

# both: compact, but also drop anything older than 30 days
--config cleanup.policy=compact,delete --config retention.ms=2592000000
Compaction is not immediate and not a guarantee of uniquenessThe most recent segment of the log, the one being written to, is never compacted, so a consumer reading the head of a compacted topic will still see multiple records for the same key. Compaction is eventual cleanup, not a unique-key constraint. Any consumer of a compacted topic must be written to handle repeats by taking the last value it sees per key.

02  Execution model

why a file beats a queue

Every performance claim about Kafka comes from four decisions: writes are sequential, reads are served from the operating system page cache, the broker keeps no per-consumer state, and records move in batches. Understanding these is what lets you defend a number in an interview instead of guessing one.

Sequential writes, cached readsDisk is fast when you stop seeking

A partition is appended to and never modified in place, so the disk head never seeks and both a spinning disk and a solid state drive run near their sequential ceiling. Written data lands in the page cache, which is memory managed by the operating system, and is flushed to disk in the background.

Consumers reading recent records therefore rarely touch the disk, because the bytes are still in the page cache from when they were written. A consumer that has fallen far behind does hit disk, which is one reason lag hurts the whole cluster and not only the slow consumer.

Because the record on disk is byte for byte what goes on the wire, the broker can hand a file region straight to the network socket instead of copying it into the application and back out.

Kafka does not force each write to diskDurability comes from replication to other machines, not from flushing every record to physical storage. The default is to let the operating system flush in the background and to rely on acks=all with min.insync.replicas so the record exists on more than one machine before it is acknowledged. Say this out loud: it is the difference between a correct answer about data loss and a confused one.

The broker keeps no reader statePosition belongs to the consumer

A traditional broker tracks which messages each consumer has taken and acknowledged, so its bookkeeping grows with the number of consumers and messages in flight. Kafka does not. The broker knows only where each partition's log ends. The consumer knows where it is, and stores that position by committing it to an ordinary internal topic.

broker knowslog end offset
per partition
minus
group knowscommitted offset
per partition
equals
lagrecords behind
per partition

Three consequences follow. Adding a tenth consumer group costs the broker almost nothing beyond the extra reads. Replaying is trivial, because rewinding means writing a smaller number into that offset topic. And an acknowledgement is not per record: committing offset 5107 asserts that everything before it is done, which is why a single bad record cannot be individually parked without extra machinery.

Batching and compressionThe unit on the wire is a batch

The producer accumulates records per partition and sends them as one compressed batch. Compressing a whole batch is far more effective than compressing each record, and the batch stays compressed on the broker's disk and is handed to consumers still compressed. The broker does not decompress it to serve reads.

The tradeoff is deliberate latency. Waiting a few milliseconds to fill a batch is what turns a per-record round trip into a bulk transfer, and it is usually the single most effective throughput change available.

SettingTypicalEffect
linger.ms0 to 20How long the producer waits to fill a batch before sending. Zero still batches under load, because records accumulate while a send is in flight.
batch.size16 to 256 KBMaximum bytes per partition batch. If it fills first, the batch ships immediately regardless of linger.
compression.typelz4 or zstdlz4 is cheap and fast, zstd gives the best ratio at more processor cost. A measurable win on both network and storage.

What a partition is on diskSegments, and why deletion is cheap

A partition is a directory of segments, each capped by size or age, not one growing file. The newest segment is open and being appended to and the rest are closed and immutable. Beside each segment sit an index from offset to byte position and an index from timestamp to offset, which is how a consumer seeks to a position or a point in time without scanning.

Retention then deletes whole closed segments rather than editing anything, which is why expiring data costs almost nothing.

Look at the actual files

# disk usage per broker, per partition
kafka-log-dirs.sh --bootstrap-server localhost:9092 \
  --describe --topic-list orders

# decode a segment: offsets, keys, timestamps, batch boundaries
kafka-dump-log.sh --print-data-log \
  --files /var/lib/kafka/data/orders-0/00000000000000000000.log

# when a segment rolls closed, which is when retention can act on it
kafka-configs.sh --bootstrap-server localhost:9092 \
  --alter --entity-type topics --entity-name orders \
  --add-config segment.bytes=1073741824,segment.ms=604800000

Planning numbers to quoteDefensible orders of magnitude

These are the figures worth carrying into an interview. Say them as orders of magnitude with the assumption attached, never as precise guarantees, because every one of them moves with record size, batching, and hardware.

QuantityOrder of magnitudeWhat moves it
Throughput per brokerhundreds of MB per secondNetwork card and sequential disk bandwidth are the ceiling. Small unbatched records collapse this by an order of magnitude.
Records per second per partitiontens of thousandsRarely the real limit. One consumer's processing rate on that partition usually binds first.
End to end latencysingle digit to tens of millisecondsProducer linger, replication acknowledgement, and consumer poll interval. Kafka is low latency, not microsecond real time.
Partitions per clusterlow hundreds of thousandsMetadata and open file handles. Much higher since the metadata quorum moved inside Kafka, but still a real budget.
Partitions per topictens to low thousandsConsumer parallelism needed, weighed against rebalance time and file handle count. More is not free.
Retentionhours to indefiniteBroker disk, or object storage if tiered storage is enabled, which decouples retention from broker disk entirely.
Say thisI would size partitions from the consumer side first. If one consumer instance processes two thousand records a second and we need twenty thousand, that is ten partitions minimum, and I would create thirty for headroom, because adding partitions later breaks key ordering.

03  The core objects

everything you will name in a design

Kafka has a small vocabulary, and these eight objects cover essentially every design answer. Each card carries the commands or settings that create and inspect the thing it describes.

Topic and partitionAdministration

CommandScopeWhat it does
kafka-topics.sh --createclusterCreates the topic with its partition count and replication factor.
kafka-topics.sh --describetopicPer partition: leader broker, replica list, in-sync replica list. The first command to run when anything looks wrong.
kafka-topics.sh --alter --partitions 12topicIncreases the partition count. One way only: it can never be decreased.
kafka-configs.sh --altertopicChanges retention, cleanup policy, segment size, and the in-sync replica floor without recreating the topic.
kafka-reassign-partitions.shclusterMoves partition replicas between brokers. This is how you add a broker and actually get data onto it.

ProducerThe write path

SettingDefaultWhat it does
acksall0 never waits, 1 waits for the leader only, all waits for every in-sync replica. Use all unless you can name why not.
enable.idempotencetrueThe broker deduplicates producer retries using a producer id and a per-partition sequence number, so a retry cannot append the same record twice.
delivery.timeout.ms120000Total time the producer keeps trying before failing the send back to your code. This, not the retry count, is the real control.
max.in.flight.requests.per.connection5Requests in flight per connection. With idempotence on, ordering holds up to five. With it off, anything above one can reorder on retry.
partitioner.classdefaultOverride to control placement directly, for example to pin a large tenant to dedicated partitions.
A send is asynchronousThe call returns a future immediately. Code that ignores that future, or never handles the callback error, silently loses records under broker pressure. Saying "and I handle the send callback, and flush before shutdown" is a cheap and real signal in an interview.

Consumer and groupThe read path

SettingDefaultWhat it does
group.idnoneNames the group. Two processes sharing it split the partitions; two processes with different ones each receive everything.
auto.offset.resetlatestWhere to start when the group has no committed offset. earliest reads all retained history, latest skips it. A frequent cause of "my consumer sees nothing".
enable.auto.committrueCommits position on a timer, which can commit records you have not finished processing. Turn it off for anything that matters.
max.poll.records500Records handed back per poll call. Lower it when per-record work is slow.
max.poll.interval.ms300000If your code does not call poll again inside this window the group assumes the member died and reassigns its partitions. The classic cause of a permanent rebalance loop.
kafka-consumer-groups.sh --describegroupCurrent offset, log end offset, and lag per partition, plus which member owns each partition.

Offsets and commitsWhere a group's position lives

Committing is itself a produce, to the compacted internal topic __consumer_offsets, keyed by group, topic, and partition. Because that topic is compacted, it holds the latest position per key, which is exactly the group's state.

ActionWhereWhat it means
commitSyncconsumerBlocks until the broker confirms the position. Slower, and the one to call after processing has actually succeeded.
commitAsyncconsumerFire and forget with a callback. Faster, and requires a final synchronous commit during shutdown.
seek and seekToBeginningconsumerMoves this consumer's position in memory, ignoring the committed value. The programmatic form of a replay.
--reset-offsets --to-datetimegroupRepositions a whole stopped group by wall clock time using the timestamp index. How a backfill is actually triggered.
A commit is a watermark, not an acknowledgementCommitting offset 5107 declares that everything below 5107 in that partition is handled. There is no way to commit 5107 while leaving 5104 outstanding. Anything that needs per-record acknowledgement must use a share group or route failures to a separate topic.

TransactionsAtomic writes across partitions

A transactional producer writes to several partitions and topics and then commits once. Consumers configured to read only committed data never see the records of an aborted transaction. This is what makes the consume, process, produce loop atomic, because the offset commit can be placed inside the same transaction as the output records.

Setting or callSideWhat it does
transactional.idproducerStable name that lets the broker fence out a zombie instance of the same logical producer after a restart.
beginTransaction and commitTransactionproducerDelimit the atomic unit. Aborting discards every buffered record across every partition involved.
sendOffsetsToTransactionproducerPuts the consumer's offset commit inside the transaction. This one call is what exactly-once stream processing is built on.
isolation.level=read_committedconsumerHides aborted records and does not read past an open transaction. Without it a consumer sees uncommitted writes.
Exactly once ends at the Kafka boundaryTransactions cover Kafka to Kafka work. The moment a consumer writes to a database, an external service, or an email provider, the guarantee is at least once again and the receiving side has to be idempotent. State that boundary explicitly, because claiming end to end exactly-once without it is the classic overclaim.

Share groupsQueue semantics, per-record acknowledgement

A share group is a second kind of group whose members cooperatively consume the same partitions instead of owning them exclusively. Consumers can outnumber partitions, and each record is acknowledged individually, with a broker-side delivery count that eventually parks a record that keeps failing.

Command or callScopeWhat it does
--group-type shareadminCreates or addresses a share group rather than a classic consumer group.
acknowledge(ACCEPT)consumerMarks this one record done, rather than everything before it.
acknowledge(RELEASE)consumerReturns the record for redelivery to any member and increments its delivery count.
acknowledge(REJECT)consumerDeclares the record unprocessable so it is never redelivered.

Reach for this when work items are independent and you want consumers to scale past the partition count. Stay with classic consumer groups when per-key ordering matters, because share groups deliberately give ordering up.

Kafka ConnectMoving data in and out without writing code

Connect is a separate worker cluster that runs source connectors, which pull from an external system into Kafka, and sink connectors, which push from Kafka into an external system. It handles offset tracking, restarts, and scaling, so the answer to "how does data get from our database into Kafka" is usually a connector rather than application code.

The source connector family that matters most in interviews is change data capture, abbreviated CDC, which tails a database's write-ahead log and emits one record per row change, keyed by primary key. The result is a compacted topic that mirrors the table.

ObjectDirectionTypical use
source connectorinto KafkaDatabase change capture, file tailing, bridging an external queue.
sink connectorout of KafkaSearch index, data warehouse, object storage, cache warming.
single message transformeitherPer-record reshaping such as masking a field or routing by content, without a stream processing job.

Kafka StreamsStateful processing as a library

Streams is a client library, not a cluster: your application links it and scales by running more instances in a consumer group. It provides joins, aggregations, and windowing, keeping local state in an embedded key-value store that is mirrored to a compacted Kafka topic so an instance can rebuild after a crash.

ConceptBacked byWhat it gives you
KStreamtopicA record by record event stream, where every record is an independent fact.
KTablecompacted topicLatest value per key. The table view of the same log.
state storechangelog topicLocal storage for aggregations and joins, restored from Kafka on restart.
windowed aggregationstate storeCounts and sums over tumbling, hopping, or session windows, with a grace period for late records.

If a question asks for counting, joining, or enriching a stream, naming Streams or an equivalent processor such as Flink is enough. Reaching for a database to do it is the answer that gets probed.

04  Keys, ordering, partitioning

the decision the rest of the design rests on

Choosing the record key is the one Kafka decision that is expensive to change later. It fixes what is ordered, what is parallel, and where the load concentrates. Everything in this cluster is a consequence of that single choice.

Adding partitions moves existing keysDerived, with the real hash values

The default partitioner takes the murmur2 hash of the key bytes, clears the sign bit, and takes the remainder against the partition count. The partition count is in the divisor, so changing it changes the answer for keys whose hash does not happen to divide the same way. Here is the full derivation for one key, growing a topic from six partitions to twelve.

1Start with the key bytes. The key is the string user-108, which the producer serializes to its eight UTF-8 bytes before hashing. key = "user-108"
2Hash the bytes with murmur2. Kafka's implementation returns a signed 32-bit integer, which for this key is negative. murmur2("user-108") = -1644591886
3Clear the sign bit. The partitioner masks with 0x7fffffff, which turns the signed value into a non-negative one so the remainder cannot be negative. -1644591886 & 0x7fffffff = 502891762
4Take the remainder against six partitions. This is the placement while the topic has its original six partitions. 502891762 % 6 = 4  →  partition 4
5Now grow the topic to twelve partitions. The key and its hash are unchanged. Only the divisor changed. 502891762 % 12 = 10  →  partition 10
6Read the consequence off those two lines. Every future record for user-108 is appended to partition 10, while its entire history sits in partition 4. Two different consumers now own the old and new records for the same entity, and there is nothing coordinating them. history in partition 4, new records in partition 10 ordering broken
7Check a key that did not move. user-42 hashes to 1459644460, and 1459644460 % 6 = 4 while 1459644460 % 12 = 4. It stays on partition 4, which is what makes this failure so unpleasant: it is silent and it hits only some keys. user-42: partition 4 before, partition 4 after unaffected
Say this before the interviewer asksPartition count is effectively permanent for a keyed topic. If you need more parallelism later, the safe options are to create a new topic with the larger count and migrate consumers across a cutover, or to over-provision partitions at creation time. Doubling in place on a topic that relies on per-key ordering is a correctness change, not a capacity change.

What the key buys youOrdering and co-location

Two guarantees come from a key, and only these two. Records with the same key always land on the same partition, so they are ordered relative to each other. Records with different keys have no ordering relationship at all, even if one was produced an hour after the other.

Key choiceOrdered perParallelism
order idone orderVery high. Millions of distinct orders spread evenly.
user idone userHigh, and it lets a consumer keep per-user state locally.
tenant idone tenantPoor if tenants differ in size. One large customer becomes a hot partition.
a constanteverythingNone. Total ordering means one partition and one consumer, which is a design smell unless the volume is genuinely tiny.
nullnothingMaximum. The producer fills one partition's batch, then switches, which keeps batches large.
Say thisI do not need global ordering, I need ordering per order, so I key by order id. That gives me ordering where it matters and full parallelism everywhere else.

Hot partitionsThe skew problem and its three fixes

Hashing distributes keys evenly, not traffic. If one key carries a hundred times the volume of the others, its partition receives a hundred times the load and one consumer instance has to keep up alone. Adding partitions does not help, because the hot key still maps to exactly one of them.

FixCostWhen it fits
composite keynarrower orderingKey by tenant plus entity instead of tenant alone. Ordering per entity survives; ordering across the tenant does not.
salted keyordering lost for that keyAppend a small random suffix to spread one hot key over several partitions. Only safe when that key's records are independent.
custom partitioneroperational complexityRoute the known large tenant to a reserved set of partitions and hash everyone else normally. Explicit and predictable.
Detect it before you fix itSkew shows up as one partition with growing lag while the rest sit near zero, which is visible in the per-partition output of kafka-consumer-groups.sh --describe. Quote that command as your detection story; it makes the fix sound operational rather than theoretical.

Choosing the partition countSize from the consumer, not the producer

Producers rarely bind first. Work the number out from how fast a single consumer instance can process a record, then leave headroom, because raising the count later is the expensive operation derived above.

sizing worked throughtarget 20,000 records per second
consumer rateone instance handles 2,000 records per secondmeasured
minimum20,000 / 2,000 = 10 instances, so 10 partitionsfloor
headroom3x for growth and for slow-consumer recovery30 partitions
check30 partitions x 3 replicas = 90 replica logs for this topicaffordable

The upper bound is not throughput but bookkeeping: every partition is an open file set on three brokers, adds to cluster metadata, and lengthens a rebalance. Tens of thousands of partitions on one cluster is ordinary, hundreds of thousands is where operational strain starts.

Create it and verify the spread

# create with headroom rather than growing later
kafka-topics.sh --bootstrap-server localhost:9092 \
  --create --topic orders --partitions 30 --replication-factor 3

# confirm each partition actually receives traffic
kafka-run-class.sh kafka.tools.GetOffsetShell \
  --bootstrap-server localhost:9092 --topic orders

05  Delivery semantics

where duplicates and losses actually come from

Almost every follow-up question in a Kafka interview is a delivery question in disguise. The three semantics are not personality types, they are specific settings with specific failure windows, and the two walkthroughs below show exactly where each window opens.

The three semantics, as configurationNot adjectives, settings

GuaranteeProducer sideConsumer sideWhat you accept
At most onceacks=0 or acks=1 with no retriescommit the offset before processingRecords can be lost and never reprocessed. Acceptable for metrics and sampled telemetry, almost nothing else.
At least onceacks=all with retriescommit the offset after processingRecords can be delivered twice. This is the default posture and the right answer for most systems, paired with an idempotent consumer.
Exactly onceenable.idempotence=true plus transactional.idisolation.level=read_committed with the offset committed inside the transactionThroughput cost and a hard boundary: the guarantee holds for Kafka to Kafka work only.
Say thisI would run at least once and make the consumer idempotent, because that survives every failure mode with a much simpler operational story than transactions. I would reach for transactions only if the pipeline is Kafka to Kafka and the processing is genuinely not idempotent.

Where a duplicate comes fromDerived, at least once

The producer already prevents its own retries from duplicating, because idempotence is on by default. The duplicate that survives is on the consumer side, and it comes from the gap between doing the work and recording that you did it.

1Consumer polls and receives records. The group's committed offset for partition 2 is 3390, so it receives records starting there. committed = 3390, received offsets 3390 to 3399
2The consumer processes record 3390. It charges a card, an irreversible side effect in an external system. payment for offset 3390 captured
3The process is killed before committing. A deploy, an out of memory kill, or a network partition. Nothing was written back to the offset topic. committed offset is still 3390
4The group rebalances and a new member takes partition 2. It reads the committed offset, which never moved. new member resumes at 3390
5Record 3390 is processed a second time. The card is charged twice. Kafka did nothing wrong: at least once is exactly what was configured. duplicate side effect charged twice
6The fix is in the consumer, not the broker. The processing step writes a row keyed by a business identifier carried on the record, under a unique constraint, so the second attempt is rejected by the database rather than prevented by Kafka. INSERT payment (idempotency_key = order-9001-capture) second attempt rejected

Where a loss comes fromDerived, at most once

Reverse the order of the same two operations and the duplicate becomes a loss. This is what automatic offset committing can do to you without any explicit configuration choice, because it commits on a timer regardless of where your processing has reached.

1Consumer polls and receives records. Same starting state: the committed offset for partition 2 is 3390. committed = 3390, received offsets 3390 to 3399
2The automatic commit timer fires. Five seconds have passed since the last commit, so the client commits the end of what it has handed out, not the end of what you have finished. committed = 3400
3The consumer is still working on 3392. Records 3392 through 3399 have been received but not processed. processed up to 3391, in flight 3392
4The process is killed. The eight in-flight records are in memory only. records 3392 to 3399 lost from memory
5A new member resumes from the committed offset. It starts at 3400 and never revisits the eight records, which are still on disk in the partition and will never be delivered to this group again. resumes at 3400 8 records silently skipped
6The fix is to control the commit. Disable automatic committing and commit synchronously after the batch has been fully processed, which converts this loss into the duplicate scenario on the previous card. enable.auto.commit=false, then commitSync() after processing no silent loss

The idempotent consumerThe pattern that makes at least once safe

Since duplicates are unavoidable in any at least once system, the consumer must make reprocessing harmless. There are three standard ways, and the right one depends on what the side effect touches.

TechniqueWhere the state livesNotes
natural idempotencenowhereThe operation is already repeatable, such as setting a value rather than incrementing one. Free when the data model allows it.
unique constraintyour databaseInsert a row keyed by a business identifier from the record inside the same transaction as the work. The second attempt violates the constraint and is discarded.
offset as a versionyour databaseStore the partition and offset with the row and only apply a record whose offset is higher than the stored one. Works well for state that is overwritten rather than accumulated.
The identifier must come from the record, not be generated on receiptA retry produces a different generated value and defeats the check. Whatever identifies the unit of work, the order id, a payment intent, an event id assigned by the producer, has to travel in the record itself so that both delivery attempts compute the same key.

The read, process, write loopThe exactly-once configuration in full

When a job consumes from one topic and produces to another and the processing genuinely cannot be made idempotent, transactions make the whole cycle atomic. Every one of these settings is required; a partial set gives you no guarantee at all.

# producer
enable.idempotence=true
transactional.id=enrich-orders-1        # stable per instance
acks=all

# consumer
isolation.level=read_committed
enable.auto.commit=false             # the transaction commits offsets

# topic
min.insync.replicas=2

# the loop
producer.beginTransaction()
producer.send(outputRecord)
producer.sendOffsetsToTransaction(offsets, consumerGroupMetadata)
producer.commitTransaction()

If Kafka Streams is doing the work, setting its processing guarantee to exactly once configures all of this for you, which is a reasonable thing to say in an interview rather than reciting the settings.

06  Interview patterns

the shapes that actually come up

These are the arrangements interviewers are listening for. Each card names the problem, the topic layout that solves it, and the follow-up question that always comes next.

Decoupling with an event logThe base pattern everything else extends

One service writes what happened; several unrelated services react. The writer does not know who reads, and a new consumer can be added later without touching the writer or replaying anything through it, because the history is already in the topic.

producercheckout service
topicorders
6 partitions
group billingown offsets
group searchown offsets
group emailown offsets

Add a consumer without touching anything else

# a new group reading all retained history from the beginning
kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic orders --group fraud --from-beginning

# prove the groups are independent
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list
The follow-up is always schemaOnce three teams consume your topic, changing the record format breaks them. The answer is a schema registry with a compatibility rule, usually backward compatibility, so that adding an optional field is allowed and removing or retyping one is rejected at produce time.

The outbox patternWriting to a database and Kafka atomically

A service cannot atomically commit a database transaction and a Kafka send. Doing both in sequence gives you the dual write problem: either the row is committed and the record is lost, or the record is published for a transaction that then rolled back.

1Write both rows in one database transaction. The business row and a row in an outbox table, committed together by the database. BEGIN; INSERT order; INSERT outbox(event); COMMIT;
2A change data capture connector tails the database log. It reads committed changes only, so it can never see the event of a rolled back transaction. connector reads the outbox table's committed inserts
3The connector publishes each outbox row to Kafka. If it crashes mid-run it resumes from its stored position and may republish, so consumers see at least once delivery. outbox row → topic orders, keyed by order id
4Consumers deduplicate on the event identifier. The identifier was assigned when the outbox row was written, so both delivery attempts carry the same value. unique constraint on event_id atomic in effect

Retry and dead letter topicsHandling the record that will not process

A classic consumer group commits a watermark, so one unprocessable record blocks its whole partition until it is dealt with. Blocking is sometimes correct, but usually the record is moved aside so the partition keeps flowing.

ordersmain topic
failsattempt 1
orders.retry.5mdelayed consumer
fails againattempt 2
orders.dlqparked for humans

Create the tiers

# retry tier and terminal tier, same partition count as the source
kafka-topics.sh --bootstrap-server localhost:9092 \
  --create --topic orders.retry.5m --partitions 6 --replication-factor 3
kafka-topics.sh --bootstrap-server localhost:9092 \
  --create --topic orders.dlq --partitions 6 --replication-factor 3

# inspect what has been parked
kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic orders.dlq --from-beginning --property print.headers=true
Moving a record aside abandons its orderingThe failed record for an entity is now behind later records for that same entity. If ordering per entity is a correctness requirement, you must block the partition instead, or park every subsequent record for that key alongside it. Say which one you are choosing and why.

Replay and backfillThe capability that justifies Kafka

Because reading does not consume, a bug fixed today can be applied to last week's traffic by moving a group's position backwards. This is the single most persuasive argument for a log over a queue, and it is worth stating explicitly in a design discussion.

Three ways to replay

# 1. rewind an existing group by time, group must be stopped
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --group billing --topic orders \
  --reset-offsets --to-datetime 2026-08-09T00:00:00.000 --execute

# 2. shadow run: a brand new group reads history with zero risk
kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic orders --group billing-v2 --from-beginning

# 3. move back a fixed number of records per partition
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --group billing --topic orders \
  --reset-offsets --shift-by -50000 --execute
Replay re-fires side effectsRewinding a group that sends email will send the email again. In production a replay is normally run as a new group writing to a new output, then cut over, rather than by rewinding the live consumer. Mention this and the idempotency requirement together.

Log compaction as a state storeThe topic becomes the table

A compacted topic keyed by entity identifier holds the latest value for every key indefinitely. A new service can subscribe, read from the beginning to build a complete local view, then stay subscribed to keep it current. This is how configuration, feature flags, and reference data are distributed to many services without a shared database.

Build one and load it

# the topic is the source of truth for current state per key
kafka-topics.sh --bootstrap-server localhost:9092 \
  --create --topic accounts --partitions 12 --replication-factor 3 \
  --config cleanup.policy=compact --config min.cleanable.dirty.ratio=0.1

# a service builds its local view by reading all of it
kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic accounts --from-beginning --property print.key=true

# delete a key: produce the key with a null value (a tombstone)
kafka-console-producer.sh --bootstrap-server localhost:9092 \
  --topic accounts --property parse.key=true \
  --property key.separator=: --property null.marker=NULL

Work queue versus event streamTwo different questions

Interviewers often say queue when they mean either. The distinction that matters is whether records are independent work items or an ordered history of something, because it decides the group type and the key.

RequirementUseWhy
Independent jobs, per-item retry, consumers scale past partitionsshare groupPer record acknowledgement and delivery counts, with no partition exclusivity.
Ordered history per entity, several independent readersconsumer groupOrdering per key and independent offsets per group.
Delay or schedule an item for laternot KafkaThere is no per-record delay. Delay tiers are separate topics with a consumer that sleeps, which is coarse. A scheduler or a sorted structure elsewhere fits better.
Priority ordering across itemsnot KafkaA log has one order, the order of arrival. Priority means separate topics per tier and a consumer that drains them in order.

Fan-out to many readersWhy one topic serves them all

The cost of an additional consumer group is the read bandwidth, not extra storage or extra broker state, because every group reads the same file and stores only its own position. Ten teams consuming one topic is normal; ten copies of a topic is a design mistake.

orders  /  three groups, one logindependent positions
group billingnear the head, processing livelag 40
group searchslightly behind, batches its writeslag 12,000
group analyticsrebuilding from the start of retentionlag 4,100,000

All three read the same segments. Only the analytics group is far enough back to be reading from disk instead of page cache, which is the specific reason a badly lagging consumer degrades everyone: it evicts warm data.

Lag as the health signalWhat to monitor and what to do

Lag is the number of records between a group's committed offset and the end of the partition. It is the one metric that captures whether the system is keeping up, and its shape tells you which failure you have.

ShapeDiagnosisAction
All partitions rising togetherunder-provisionedAdd consumer instances up to the partition count, then look at per-record cost.
One partition rising alonehot key or stuck consumerCheck for skew, and check whether that member is failing and rejoining.
Lag flat but non-zerokeeping up with a backlogNothing urgent. Throughput matches arrival rate but the deficit is not being repaid.
Sawtoothrebalance loopProcessing exceeds the poll interval. Reduce records per poll or raise the interval.
Say thisI alarm on lag in time, not in records. Two million records of lag means nothing on its own; two minutes behind on a payment pipeline is an incident, and the same two minutes on an analytics pipeline is not.

07  Cluster and operations

what happens when a machine dies

A Kafka cluster is a set of brokers that hold partition replicas, plus a metadata quorum that decides who leads what. Interviewers probe here to find out whether you have run this or only read about it.

Brokers and the metadata quorumKRaft, the built-in consensus layer

Cluster metadata, meaning which topics exist, which broker leads which partition, and which replicas are in sync, is itself stored in an internal Kafka log replicated by a Raft quorum of controller nodes. The name for this mode is KRaft, short for Kafka Raft.

This replaced the external coordination service that older deployments ran alongside Kafka. If you are asked about ZooKeeper, the accurate answer is that it was removed entirely in the 4.0 release and current clusters do not run it.

RoleCountHolds
controller3 or 5The metadata log and the elected active controller. An odd number, because it is a Raft quorum.
brokeras many as neededPartition replicas and the client connections. Scales with data and throughput.
combined modesmall clusters onlyOne process serving both roles. Convenient in development, discouraged in production.

Leader failure, step by stepDerived

1Steady state. Partition 0 has replicas on brokers 1, 2, and 3, with broker 1 as leader and all three in sync. leader 1, ISR [1, 2, 3]
2Broker 1 stops responding. The controller notices the session has expired. ISR [2, 3], leader unavailable
3The controller elects a new leader from the in-sync set. Broker 2 is promoted, and because it was in sync it holds every record that was ever acknowledged. leader 2, ISR [2, 3] no acknowledged record lost
4Clients discover the change and reconnect. Producers and consumers refresh metadata and send to broker 2. Client libraries do this automatically, so the application sees a brief pause rather than an error. produce and fetch resume against broker 2
5Broker 1 returns. It rejoins as a follower, truncates anything it had beyond the new leader's log, and catches up before being readmitted to the in-sync set. leader 2, ISR [2, 3, 1]
The unclean election switchIf every in-sync replica is gone, Kafka by default refuses to elect an out of sync replica and the partition goes offline, choosing consistency over availability. Enabling unclean leader election trades that for availability and silently discards acknowledged records. Name the tradeoff; do not enable it by reflex.

RebalancingThe most common source of operational pain

Whenever a member joins or leaves a group, partitions are reassigned. The old protocol stopped every consumer during that reassignment, so one restarting member paused the entire group. Two mechanisms fix this and both are worth naming.

MechanismEffectWhen to mention it
cooperative rebalancingincrementalOnly the partitions that actually move are revoked, so unaffected consumers keep processing throughout.
static membershipno rebalanceA member declares a stable instance identifier, so a rolling restart within the session timeout reclaims its own partitions without triggering reassignment.
server-side assignmentfaster and lighterThe newer consumer group protocol moves assignment computation to the broker, removing the stop-the-world join barrier entirely.

See who owns what right now

# the MEMBER-ID and HOST columns show current ownership
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --describe --group billing --members --verbose

Sizing and placementNumbers you can defend

storage worked through100k records/s, 1 KB each, 7 day retention
raw ingest100,000 x 1 KB = 100 MB per secondbefore replication
per day100 MB x 86,400 = 8.64 TB per dayone copy
7 day retention8.64 x 7 = 60.5 TBone copy
replication factor 360.5 x 3 = 181.4 TBcluster total
at 70% disk target181.4 / 0.7 = 259 TB provisionedthe number to quote

Compression usually takes the raw figure down by a factor of two to five for text-like payloads, so quoting the uncompressed number and then saying you expect compression to reduce it is the honest version. Tiered storage changes this calculation entirely by moving closed segments to object storage, leaving only recent data on broker disk.

Rack awareness is a one-line answer that sounds seniorConfigure the broker's rack or availability zone identifier and Kafka spreads a partition's replicas across zones, so losing a whole zone leaves the partition available. It costs cross-zone network on replication, which is a real bill worth acknowledging.

08  Configuration cheat sheet

the settings worth memorizing

Grouped by which failure they prevent, because that is how the follow-up question is usually phrased.

Do not lose data

SettingValue
acksall
min.insync.replicas2, with replication factor 3
enable.idempotencetrue
enable.auto.commitfalse, commit after processing
unclean.leader.election.enablefalse

Do not duplicate

SettingValue
transactional.idstable per producer instance
isolation.levelread_committed
consumer-side dedup keycarried in the record, never generated on receipt

Go faster

SettingValue
linger.ms5 to 20, the cheapest throughput win
batch.size64 to 256 KB
compression.typelz4 or zstd
fetch.min.bytesraise it to trade latency for fewer round trips
partitionssized from consumer throughput, with headroom

Stop rebalancing

SettingValue
max.poll.recordslower it until a batch finishes well inside the interval
max.poll.interval.msraise only after checking why processing is slow
group.instance.idset it, for static membership across restarts

Guarantee matrix

You wantYou configure
Ordering per entityKey by that entity, and never grow partitions in place.
Ordering globallyOne partition, therefore one consumer. Usually the wrong requirement.
No loss on broker failureacks=all with min.insync.replicas=2 and replication factor 3.
No duplicate side effectsIdempotent consumer keyed on a record-carried identifier.
Atomic Kafka to KafkaTransactions with read_committed on the consumer.
Per-record acknowledgementShare group, giving up per-key ordering.
Current state per key, foreverCompacted topic keyed by entity identifier.

09  What to say out loud

delivery

Knowing Kafka and interviewing well on Kafka are different skills. This cluster is the second one.

Standard questions and the Kafka move each one wantsMapping

QuestionWhat Kafka is doing in your answerThe key
Design a payment systemDurable event log between services, with the outbox pattern so the database write and the published event cannot diverge.payment id
Design a news feedFan-out writes as events, consumed by a service that materializes each follower's feed. Replayable when the ranking changes.author id
Design a ride hailing systemDriver location updates as a high volume stream, keyed so one driver's positions stay ordered.driver id
Design a chat systemPersistence and cross-service fan-out of messages, keyed per conversation for ordering. The last hop to the device is a socket, not Kafka.conversation id
Design a metrics or logging pipelineThe buffer that absorbs bursts and feeds several sinks: a warehouse, a search index, and alerting, each as its own group.null, for spread
Design a web crawlerFrontier of URLs to fetch as a queue, which is the case for a share group. Deduplicate before publishing, not after.domain, to rate limit per host
Design an order or ticket systemState changes per order as an ordered log, with a compacted topic holding current state per order.order id
Design a notification serviceDecoupling the trigger from delivery, with retry tiers and a dead letter topic for addresses that keep failing.user id
Design a data warehouse ingestChange data capture from operational databases into compacted topics, then a sink connector into the warehouse.primary key
Design a fraud or anomaly detectorStream processing with windowed aggregation over the event stream, keyed by the entity being scored.account id
The follow-up is the same in almost every one of theseExpect "what happens if a consumer processes a record twice" and "what happens when one key is far hotter than the others". Having the idempotent consumer pattern and the hot partition fixes ready is worth more than any additional feature knowledge.

When not to reach for KafkaSaying this earns credibility

SituationBetter fit
Request and response between two servicesA synchronous call. A log is not a substitute for an interface that returns a value.
Per-message delay or schedulingA scheduler or a time-ordered structure. Kafka has no per-record timer.
Priority orderingA broker with priority queues, or separate topics drained in order.
Low volume task queue in a small systemA database-backed queue or a simpler broker. Kafka's operational cost is real.
Point to point routing with complex rulesA message broker with exchanges and bindings.
Queries over the dataA database. A log is read sequentially, not searched.
Say thisI would not put Kafka in this design. The volume is a few hundred messages a minute and there is one consumer, so a queue in the database we already run is less to operate and easier to reason about.

Questions to ask before designingRequirements that change the answer

  • What has to stay ordered, and ordered relative to what? This sets the key and therefore everything downstream.
  • Is a duplicate acceptable, and what does it cost? A duplicate email is an annoyance; a duplicate charge is an incident. This decides how hard you work on idempotency.
  • How far back does anyone need to read? Retention and whether tiered storage is worth it fall out of this.
  • How many independent consumers, now and later? This decides whether the topic is a service's private channel or a shared contract needing schema governance.
  • What is the acceptable end to end latency? Separates a Kafka answer from a real-time answer, and sets the batching settings.
  • What is the peak to average ratio? Absorbing bursts is a primary reason to introduce a log at all.

Delivery notesHow to sound like you have run this

  • Name the key first, before drawing anything. Ordering and parallelism both follow from it, so deciding it early makes the rest of the design coherent.
  • Say partition count is a one-way door. Mentioning that growing it moves existing keys is a strong signal, and it is the derivation on the board in cluster 04.
  • Quote the durability pair together. acks=all and min.insync.replicas=2 mean nothing separately.
  • Draw consumer groups, not consumers. The group is the unit of parallelism and the unit of position.
  • Volunteer the duplicate. Say at least once, then immediately say how the consumer is idempotent. Waiting to be asked looks like you had not considered it.
  • Bound the exactly-once claim. Kafka to Kafka only, and only with transactions on both sides.
  • Use lag as your monitoring answer. One metric, measured in time, with a per-partition breakdown for diagnosis.

Version notesCurrent as of this board

  • The 4.x line is current, with 4.3 released in May 2026 and a 4.3.1 bug fix release in June 2026.
  • Version 4.0, released in March 2025, removed the external coordination service entirely. Clusters now run the built-in Raft metadata quorum, so ZooKeeper is history rather than an alternative mode.
  • Share groups, the queue semantics described in cluster 03, arrived as an early access feature in 4.0 and became production ready in 4.2 in February 2026. Treat them as usable but new: many teams have not adopted them, so present them as an option rather than the default.
  • The newer consumer group protocol moves partition assignment to the broker and removes the stop-the-world join barrier. If a discussion turns to rebalance pain, this is the current answer.
  • Tiered storage moves closed segments to object storage, which decouples retention from broker disk and changes the sizing math in cluster 07 substantially.
  • Managed offerings differ from the open source project in defaults and available features, so it is fine to say the exact deployment target decides.