<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Renga&#x27;s Learning - kafka</title>
    <subtitle>Blog to add my leaning on tech contents</subtitle>
    <link rel="self" type="application/atom+xml" href="https://blog.rengaonline.in/tags/kafka/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://blog.rengaonline.in"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-06-22T00:00:00+00:00</updated>
    <id>https://blog.rengaonline.in/tags/kafka/atom.xml</id>
    <entry xml:lang="en">
        <title>Kafka Notes</title>
        <published>2026-06-22T00:00:00+00:00</published>
        <updated>2026-06-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Renganatha
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://blog.rengaonline.in/blog/kafka-notes/"/>
        <id>https://blog.rengaonline.in/blog/kafka-notes/</id>
        
        <content type="html" xml:base="https://blog.rengaonline.in/blog/kafka-notes/">&lt;h2 id=&quot;broker-zookeeper-and-kraft&quot;&gt;Broker, ZooKeeper, and KRaft&lt;&#x2F;h2&gt;
&lt;p&gt;A &lt;strong&gt;broker&lt;&#x2F;strong&gt; is a Kafka server that stores topic partitions, handles producer writes, and serves consumer reads. A Kafka cluster is simply a group of brokers. One broker per partition acts as the &lt;strong&gt;partition leader&lt;&#x2F;strong&gt; — it handles all reads and writes for that partition while the others replicate it as followers.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;ZooKeeper (legacy — removed in Kafka 4.0)&lt;&#x2F;strong&gt; was the external coordination service Kafka relied on to store cluster metadata: which brokers are alive, who is the controller, and which replicas are in sync. One broker was elected &lt;strong&gt;Controller&lt;&#x2F;strong&gt; by racing to write a node in ZooKeeper; the Controller then managed partition leadership changes whenever brokers joined or left. The downside was an extra cluster to operate and a slow metadata path that went through ZooKeeper.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;KRaft (production-ready since Kafka 3.3)&lt;&#x2F;strong&gt; replaced ZooKeeper by embedding the Raft consensus algorithm directly into Kafka. A small quorum of controller nodes (typically 3 or 5) elects an &lt;strong&gt;Active Controller&lt;&#x2F;strong&gt; among themselves and replicates all cluster metadata through an internal topic called &lt;code&gt;__cluster_metadata&lt;&#x2F;code&gt;. Brokers receive metadata updates as a stream from the Active Controller — no separate ZooKeeper process needed.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;producer&quot;&gt;Producer&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;replication-factor-and-in-sync-replicas&quot;&gt;Replication Factor and In-Sync Replicas&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;Replication factor&lt;&#x2F;strong&gt; is the total number of copies Kafka keeps for each partition — one leader and the rest as followers. With a replication factor of 3, there is 1 leader + 2 followers.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;In-Sync Replicas (ISR)&lt;&#x2F;strong&gt; is the subset of those replicas that are currently caught up with the leader. A replica stays in the ISR as long as it has fetched up to the leader&#x27;s latest offset within the &lt;code&gt;replica.lag.time.max.ms&lt;&#x2F;code&gt; window (default 30 s). If a follower falls behind — due to network issues, GC pause, or broker slowness — it is removed from the ISR. When it catches up again, it is re-added.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;min.insync.replicas&lt;&#x2F;code&gt; (min.ISR)&lt;&#x2F;strong&gt; is the key knob that connects the two:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;It sets the minimum number of replicas that must be in the ISR for the partition to accept writes &lt;strong&gt;when &lt;code&gt;acks=all&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;If the live ISR count drops below this value, the partition rejects writes with &lt;code&gt;NotEnoughReplicasException&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Typical production setup:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Config&lt;&#x2F;th&gt;&lt;th&gt;Value&lt;&#x2F;th&gt;&lt;th&gt;Meaning&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;replication.factor&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;3&lt;&#x2F;td&gt;&lt;td&gt;3 copies total&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;min.insync.replicas&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;2&lt;&#x2F;td&gt;&lt;td&gt;At least 2 must acknowledge&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;acks&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;all&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Producer waits for all ISR replicas&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;This tolerates &lt;strong&gt;1 broker failure&lt;&#x2F;strong&gt; while maintaining strong durability. You never want &lt;code&gt;min.insync.replicas == replication.factor&lt;&#x2F;code&gt; in production — if any single replica is unavailable the partition becomes unwritable.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;The relationship in plain terms:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;replication.factor = 3   →  [Leader | Follower-1 | Follower-2]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ISR (all healthy)        →  [Leader | Follower-1 | Follower-2]  ✓ 3 in ISR&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Follower-2 crashes:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ISR shrinks              →  [Leader | Follower-1]               ✓ 2 in ISR ≥ min.ISR (2), writes still allowed&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Follower-1 also crashes:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ISR shrinks              →  [Leader]                            ✗ 1 in ISR &amp;lt; min.ISR (2), writes blocked&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;acknowledgements-acks&quot;&gt;Acknowledgements (&lt;code&gt;acks&lt;&#x2F;code&gt;)&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0&lt;&#x2F;code&gt; — fire and forget; no acknowledgement waited for (fastest, no durability guarantee).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;1&lt;&#x2F;code&gt; — leader writes to its local log and acknowledges; followers may not have replicated yet (leader loss = data loss).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;all&lt;&#x2F;code&gt; — leader waits until all ISR replicas have written the batch before acknowledging; combined with &lt;code&gt;min.insync.replicas&lt;&#x2F;code&gt;, this is the strongest durability guarantee.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;linger-ms-and-batch-size&quot;&gt;&lt;code&gt;linger.ms&lt;&#x2F;code&gt; and &lt;code&gt;batch.size&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;The producer accumulates records into batches before sending to improve throughput.&lt;&#x2F;li&gt;
&lt;li&gt;A batch is sent when either &lt;code&gt;batch.size&lt;&#x2F;code&gt; bytes are filled (default 16 KB) &lt;strong&gt;or&lt;&#x2F;strong&gt; &lt;code&gt;linger.ms&lt;&#x2F;code&gt; has elapsed (default 0 ms).&lt;&#x2F;li&gt;
&lt;li&gt;Setting &lt;code&gt;linger.ms=5&lt;&#x2F;code&gt; and a larger &lt;code&gt;batch.size&lt;&#x2F;code&gt; is a common throughput tuning: the producer waits up to 5 ms to fill a bigger batch before flushing.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;idempotent-producer&quot;&gt;Idempotent Producer&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;the-problem-it-solves&quot;&gt;The Problem It Solves&lt;&#x2F;h4&gt;
&lt;p&gt;Without idempotence, a transient network failure during a produce request can cause a duplicate:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Producer sends a batch → broker writes it → broker acknowledgement is lost in transit.&lt;&#x2F;li&gt;
&lt;li&gt;Producer times out and retries → broker writes the same batch &lt;strong&gt;again&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Consumer sees the message twice.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;how-it-works-internally&quot;&gt;How It Works Internally&lt;&#x2F;h4&gt;
&lt;p&gt;Enable with &lt;code&gt;enable.idempotence=true&lt;&#x2F;code&gt;. Kafka automatically enforces: &lt;code&gt;acks=all&lt;&#x2F;code&gt;, &lt;code&gt;retries=Integer.MAX_VALUE&lt;&#x2F;code&gt;, &lt;code&gt;max.in.flight.requests.per.connection ≤ 5&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Producer ID (PID):&lt;&#x2F;strong&gt; On first connect, the producer sends an &lt;code&gt;InitProducerId&lt;&#x2F;code&gt; request to the broker. The broker assigns a globally unique &lt;strong&gt;Producer ID (PID)&lt;&#x2F;strong&gt; and returns it. The PID identifies this producer session.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Sequence Numbers:&lt;&#x2F;strong&gt; The producer maintains a monotonically increasing &lt;strong&gt;sequence number&lt;&#x2F;strong&gt; per &lt;code&gt;(PID, TopicPartition)&lt;&#x2F;code&gt;, starting at 0. Every record gets stamped with the current sequence number before it is sent. The sequence number is part of the message metadata on the wire.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Broker-side deduplication:&lt;&#x2F;strong&gt; The broker tracks the &lt;strong&gt;last committed sequence number&lt;&#x2F;strong&gt; for every &lt;code&gt;(PID, partition)&lt;&#x2F;code&gt; pair.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;If a batch arrives with an &lt;strong&gt;already-seen sequence number&lt;&#x2F;strong&gt; → silent deduplicate, return success to producer.&lt;&#x2F;li&gt;
&lt;li&gt;If a batch arrives &lt;strong&gt;in order&lt;&#x2F;strong&gt; (seq = last + 1) → commit, advance the tracked sequence.&lt;&#x2F;li&gt;
&lt;li&gt;If a batch arrives with a &lt;strong&gt;gap&lt;&#x2F;strong&gt; (seq &amp;gt; last + 1) → reject with &lt;code&gt;OutOfOrderSequenceException&lt;&#x2F;code&gt;; this signals a bug, not a retry scenario.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This means retries are safe — the producer resends the same batch with the same sequence numbers and the broker simply ignores the duplicate.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;internal-batching-behaviour&quot;&gt;Internal Batching Behaviour&lt;&#x2F;h4&gt;
&lt;p&gt;The producer client has two internal components:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;RecordAccumulator&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — an in-memory buffer organised as a map of &lt;code&gt;TopicPartition → Deque&amp;lt;RecordBatch&amp;gt;&lt;&#x2F;code&gt;. &lt;code&gt;send()&lt;&#x2F;code&gt; calls are non-blocking: they append the record to the appropriate batch in the accumulator and return a &lt;code&gt;Future&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;Sender&lt;&#x2F;code&gt; thread&lt;&#x2F;strong&gt; — a single background thread that drains the accumulator and dispatches batches to brokers over the network.&lt;&#x2F;p&gt;
&lt;p&gt;With idempotent producer the Sender adds two guarantees:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sequence assignment happens in the Sender thread&lt;&#x2F;strong&gt;, not the calling thread, so sequence numbers are assigned in the exact order batches are drained from the accumulator. This keeps ordering consistent even when multiple application threads call &lt;code&gt;send()&lt;&#x2F;code&gt; concurrently.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;In-flight tracking per partition&lt;&#x2F;strong&gt;: the Sender keeps a queue of in-flight batches per &lt;code&gt;(broker, partition)&lt;&#x2F;code&gt;. On a retriable error, the batch is &lt;strong&gt;re-queued at the front&lt;&#x2F;strong&gt; of the deque with its original sequence numbers intact — the batch object is not recreated. This preserves the ordering the broker expects.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Application thread(s)         Sender thread           Broker&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   send(record) ──►  RecordAccumulator  ──batch──►  seq check&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                      [TP-A: [batch0]]              deduplicate&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                      [TP-B: [batch1]]              commit &amp;amp; ack&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                           │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                     on retry: requeue&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                     batch at front with&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                     same PID + seq&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;max.in.flight.requests.per.connection=5&lt;&#x2F;code&gt; is safe:&lt;&#x2F;strong&gt; With plain retries (no idempotence) having multiple in-flight batches means a retry of batch-1 could arrive after batch-2 has already been committed — messages reorder. With idempotence the broker rejects any batch that arrives with a sequence number that creates a gap, so reordering is caught at the broker. Five in-flight batches is a deliberate balance: enough parallelism for throughput, small enough to bound the sequence tracking window on the broker.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;consumer&quot;&gt;Consumer&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;consumer-group-and-partition-assignment&quot;&gt;Consumer Group and Partition Assignment&lt;&#x2F;h3&gt;
&lt;p&gt;A &lt;strong&gt;consumer group&lt;&#x2F;strong&gt; is a set of consumer instances sharing a single &lt;code&gt;group.id&lt;&#x2F;code&gt;. Kafka guarantees that each partition of a topic is assigned to &lt;strong&gt;exactly one consumer&lt;&#x2F;strong&gt; within the group at any given time — this is the fundamental unit of parallelism.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scaling rule&lt;&#x2F;strong&gt;: the maximum useful parallelism equals the number of partitions. Adding more consumers than partitions leaves the extras idle.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Group Coordinator&lt;&#x2F;strong&gt;: one broker acts as the group coordinator, tracking membership and managing rebalances. The coordinator is determined by hashing the &lt;code&gt;group.id&lt;&#x2F;code&gt; to a partition of the internal &lt;code&gt;__consumer_offsets&lt;&#x2F;code&gt; topic.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Group Leader&lt;&#x2F;strong&gt;: one consumer in the group (the first to join) is elected leader by the coordinator. The leader runs the &lt;strong&gt;partition assignment algorithm&lt;&#x2F;strong&gt; (RoundRobin, Range, Sticky, or a custom assignor) and sends the result back to the coordinator, which distributes assignments to all members.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Rebalance&lt;&#x2F;strong&gt; is triggered when a consumer joins, leaves, or crashes, or when topic partitions change. During a rebalance all consumption pauses. The &lt;strong&gt;Sticky Assignor&lt;&#x2F;strong&gt; minimises disruption by keeping existing assignments unchanged and only moving partitions that have to move.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Message ordering&lt;&#x2F;strong&gt; is guaranteed &lt;strong&gt;within a partition&lt;&#x2F;strong&gt; — consumers receive records in the exact order they were produced to that partition. There is no ordering guarantee across partitions. Since each partition is assigned to exactly one consumer in a group, that consumer processes its partition&#x27;s records in offset order.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;batch-consumer&quot;&gt;Batch Consumer&lt;&#x2F;h3&gt;
&lt;p&gt;Consumers do not pull one record at a time — &lt;code&gt;poll(duration)&lt;&#x2F;code&gt; returns a batch of up to &lt;code&gt;max.poll.records&lt;&#x2F;code&gt; records (default 500) in a single call.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;while&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;true&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;    ConsumerRecords&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;font-style: italic;&quot;&gt;K&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;font-style: italic;&quot;&gt; V&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt; records&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; consumer.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;poll&lt;&#x2F;span&gt;&lt;span&gt;(Duration.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;ofMillis&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;100&lt;&#x2F;span&gt;&lt;span&gt;));&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    for&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;ConsumerRecord&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;font-style: italic;&quot;&gt;K&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;font-style: italic;&quot;&gt; V&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt; record&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; :&lt;&#x2F;span&gt;&lt;span&gt; records) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;        process&lt;&#x2F;span&gt;&lt;span&gt;(record);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    consumer.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;commitSync&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Key configs:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Config&lt;&#x2F;th&gt;&lt;th&gt;Default&lt;&#x2F;th&gt;&lt;th&gt;Purpose&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;max.poll.records&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;500&lt;&#x2F;td&gt;&lt;td&gt;Max records returned per &lt;code&gt;poll()&lt;&#x2F;code&gt; call&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;fetch.min.bytes&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;1&lt;&#x2F;td&gt;&lt;td&gt;Broker waits until this many bytes are available before responding&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;fetch.max.wait.ms&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;500 ms&lt;&#x2F;td&gt;&lt;td&gt;Max time broker will wait to satisfy &lt;code&gt;fetch.min.bytes&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;max.partition.fetch.bytes&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;1 MB&lt;&#x2F;td&gt;&lt;td&gt;Max bytes fetched per partition per request&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Tuning &lt;code&gt;fetch.min.bytes&lt;&#x2F;code&gt; and &lt;code&gt;fetch.max.wait.ms&lt;&#x2F;code&gt; together controls the latency-throughput tradeoff on the broker side: higher &lt;code&gt;fetch.min.bytes&lt;&#x2F;code&gt; means larger batches but more latency before the first record arrives.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;manual-acknowledgement&quot;&gt;Manual Acknowledgement&lt;&#x2F;h3&gt;
&lt;p&gt;By default Kafka auto-commits offsets in the background every &lt;code&gt;auto.commit.interval.ms&lt;&#x2F;code&gt; (default 5 s). This can cause &lt;strong&gt;at-most-once&lt;&#x2F;strong&gt; semantics: if the consumer crashes after auto-commit but before finishing processing, records are skipped.&lt;&#x2F;p&gt;
&lt;p&gt;Set &lt;code&gt;enable.auto.commit=false&lt;&#x2F;code&gt; and commit explicitly for &lt;strong&gt;at-least-once&lt;&#x2F;strong&gt; guarantees.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;commitSync()&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — blocks until the broker confirms the commit. Safe but slows throughput.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;commitAsync()&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — non-blocking; fires the commit and provides a callback for success&#x2F;failure. Higher throughput, but a failed async commit is not retried by default — retrying it naively can cause out-of-order commits (a later offset commits first, then a retry commits an earlier offset over it, losing progress).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Common pattern&lt;&#x2F;strong&gt;: use &lt;code&gt;commitAsync()&lt;&#x2F;code&gt; in the hot path and &lt;code&gt;commitSync()&lt;&#x2F;code&gt; only in the &lt;code&gt;finally&lt;&#x2F;code&gt; block on shutdown to ensure the last offsets are flushed.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;try&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    while&lt;&#x2F;span&gt;&lt;span&gt; (running) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;        var&lt;&#x2F;span&gt;&lt;span&gt; records&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; consumer.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;poll&lt;&#x2F;span&gt;&lt;span&gt;(Duration.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;ofMillis&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;100&lt;&#x2F;span&gt;&lt;span&gt;));&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;        process&lt;&#x2F;span&gt;&lt;span&gt;(records);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        consumer.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;commitAsync&lt;&#x2F;span&gt;&lt;span&gt;((offsets, ex)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; -&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; if&lt;&#x2F;span&gt;&lt;span&gt; (ex &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;!=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; null&lt;&#x2F;span&gt;&lt;span&gt;) log.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;warn&lt;&#x2F;span&gt;&lt;span&gt;(...); });&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; finally&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    consumer.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;commitSync&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;   &#x2F;&#x2F; best-effort flush on shutdown&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    consumer.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;close&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;nack-retry-single-record-failure&quot;&gt;Nack &#x2F; Retry (Single-Record Failure)&lt;&#x2F;h3&gt;
&lt;p&gt;Kafka has no native negative-acknowledgement. When a single record fails processing:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Do not commit&lt;&#x2F;strong&gt; the offset of the failed record — it will be re-delivered on the next &lt;code&gt;poll()&lt;&#x2F;code&gt; after a restart or rebalance.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Seek back&lt;&#x2F;strong&gt; explicitly: call &lt;code&gt;consumer.seek(partition, failedOffset)&lt;&#x2F;code&gt; to re-position the consumer to the failed record without waiting for a crash&#x2F;rebalance.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Route to a retry topic&lt;&#x2F;strong&gt;: instead of blocking, publish the failed record to a &lt;code&gt;&amp;lt;topic&amp;gt;.retry&lt;&#x2F;code&gt; topic and commit the original offset. A separate consumer processes the retry topic with backoff logic.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Option 2 (seek back) is simple but blocks the partition — all records after the failed one are stuck until the failure is resolved. Option 3 (retry topic) is preferred for production as it decouples failure handling from the main consumer&#x27;s throughput.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;batch-acknowledge&quot;&gt;Batch Acknowledge&lt;&#x2F;h3&gt;
&lt;p&gt;When processing records as a batch (e.g., bulk-inserting into a database), commit only after the &lt;strong&gt;entire batch&lt;&#x2F;strong&gt; succeeds:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;var&lt;&#x2F;span&gt;&lt;span&gt; records&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; consumer.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;poll&lt;&#x2F;span&gt;&lt;span&gt;(Duration.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;ofMillis&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;100&lt;&#x2F;span&gt;&lt;span&gt;));&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;bulkInsert&lt;&#x2F;span&gt;&lt;span&gt;(records);&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      &#x2F;&#x2F; if this throws, do NOT commit&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;consumer.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;commitSync&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    &#x2F;&#x2F; commit only on success&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This gives at-least-once semantics at batch granularity. If the bulk insert fails partway through, the whole batch is reprocessed on the next poll — so &lt;code&gt;bulkInsert&lt;&#x2F;code&gt; should be idempotent (e.g., use upserts or de-duplicate on the database side).&lt;&#x2F;p&gt;
&lt;p&gt;To commit at record granularity within the batch, use &lt;code&gt;commitSync(Map&amp;lt;TopicPartition, OffsetAndMetadata&amp;gt;)&lt;&#x2F;code&gt; with a precise offset map rather than committing all assigned partitions at once.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;heartbeat-thread-and-liveness-detection&quot;&gt;Heartbeat Thread and Liveness Detection&lt;&#x2F;h3&gt;
&lt;p&gt;The consumer runs a &lt;strong&gt;dedicated background heartbeat thread&lt;&#x2F;strong&gt; separate from the application thread that calls &lt;code&gt;poll()&lt;&#x2F;code&gt;. Liveness has two independent signals:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;session.timeout.ms&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; (default 45 s) — if the broker does not receive a heartbeat within this window, it considers the consumer dead and triggers a rebalance. The heartbeat thread sends heartbeats every &lt;code&gt;heartbeat.interval.ms&lt;&#x2F;code&gt; (default 3 s; recommended: ≤ &lt;code&gt;session.timeout.ms &#x2F; 3&lt;&#x2F;code&gt;).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;max.poll.interval.ms&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; (default 5 min) — if the application thread does not call &lt;code&gt;poll()&lt;&#x2F;code&gt; again within this window, the consumer is also considered dead. This catches the case where the heartbeat thread is alive but the application is stuck processing a record and never returns to the poll loop.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Heartbeat thread    ──heartbeat──►  Group Coordinator  ← session.timeout.ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Application thread  ──poll()──►     (same coordinator)  ← max.poll.interval.ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Both timeouts must be satisfied to keep the consumer in the group. If &lt;code&gt;max.poll.interval.ms&lt;&#x2F;code&gt; is exceeded, the consumer voluntarily leaves the group and triggers a rebalance — even if the heartbeat thread is still healthy.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Tuning guidance&lt;&#x2F;strong&gt;: if record processing is genuinely slow (e.g., calling an external API per record), increase &lt;code&gt;max.poll.interval.ms&lt;&#x2F;code&gt; or reduce &lt;code&gt;max.poll.records&lt;&#x2F;code&gt; so each &lt;code&gt;poll()&lt;&#x2F;code&gt; returns fewer records to process within the window.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;non-blocking-retry-pattern-retry-topics&quot;&gt;Non-Blocking Retry Pattern (Retry Topics)&lt;&#x2F;h3&gt;
&lt;p&gt;The retry-topic pattern decouples failure handling from the main consumer loop, keeping throughput high on the happy path.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;main-topic  ──►  [Consumer]  ──failure──►  topic.retry-1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                     │                          │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                   success              [Retry Consumer, delay=30s]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                     │                          │──failure──►  topic.retry-2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                  commit                        │                    │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                                             success        [Retry Consumer, delay=5m]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                                              commit               │──failure──►  topic.dlt&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;Main consumer catches a processing exception → publishes the original record (with added headers: &lt;code&gt;retry-count&lt;&#x2F;code&gt;, &lt;code&gt;original-topic&lt;&#x2F;code&gt;, &lt;code&gt;error-message&lt;&#x2F;code&gt;) to &lt;code&gt;topic.retry-1&lt;&#x2F;code&gt; → commits the offset and continues.&lt;&#x2F;li&gt;
&lt;li&gt;A dedicated retry consumer subscribes to &lt;code&gt;topic.retry-1&lt;&#x2F;code&gt;, applies a delay (&lt;code&gt;Thread.sleep&lt;&#x2F;code&gt; or a scheduled poll pause), and retries the record.&lt;&#x2F;li&gt;
&lt;li&gt;On repeated failure the record is promoted to &lt;code&gt;topic.retry-2&lt;&#x2F;code&gt; (longer delay), and eventually to a &lt;strong&gt;Dead Letter Topic (DLT)&lt;&#x2F;strong&gt; for manual inspection.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Spring Kafka&#x27;s &lt;code&gt;@RetryableTopic&lt;&#x2F;code&gt; implements this pattern automatically with configurable backoff and DLT routing.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;circuit-breaker&quot;&gt;Circuit Breaker&lt;&#x2F;h3&gt;
&lt;p&gt;When a downstream dependency (database, external API) is degraded, continuing to consume and fail records wastes resources and floods retry topics. A circuit breaker pauses consumption during outages.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;States&lt;&#x2F;strong&gt;: Closed (normal) → Open (paused) → Half-Open (testing recovery).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Kafka-specific implementation&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;&#x2F;&#x2F; On repeated failures, open the circuit:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;consumer.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;pause&lt;&#x2F;span&gt;&lt;span&gt;(consumer.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;assignment&lt;&#x2F;span&gt;&lt;span&gt;());&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;   &#x2F;&#x2F; stop fetching, stay in group&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;&#x2F;&#x2F; poll() must still be called to send heartbeats and avoid max.poll.interval.ms breach:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;while&lt;&#x2F;span&gt;&lt;span&gt; (circuitOpen) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    consumer.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;poll&lt;&#x2F;span&gt;&lt;span&gt;(Duration.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;ofMillis&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;100&lt;&#x2F;span&gt;&lt;span&gt;));&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  &#x2F;&#x2F; returns empty — partitions are paused&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;dependencyHealthy&lt;&#x2F;span&gt;&lt;span&gt;()) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        consumer.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;resume&lt;&#x2F;span&gt;&lt;span&gt;(consumer.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;assignment&lt;&#x2F;span&gt;&lt;span&gt;());&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        circuitOpen &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;consumer.pause()&lt;&#x2F;code&gt; stops the broker from returning records for those partitions but keeps the consumer in the group and heartbeating. This is critical — if you simply stop calling &lt;code&gt;poll()&lt;&#x2F;code&gt; you will breach &lt;code&gt;max.poll.interval.ms&lt;&#x2F;code&gt; and trigger a rebalance.&lt;&#x2F;p&gt;
&lt;p&gt;The circuit opens after N consecutive failures (configurable threshold), waits a cooldown period, then enters half-open state where a single test record is processed to confirm recovery before fully resuming.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;transactions&quot;&gt;Transactions&lt;&#x2F;h2&gt;
&lt;p&gt;Kafka transactions give you &lt;strong&gt;exactly-once semantics (EOS)&lt;&#x2F;strong&gt; across produce and consume operations. Without understanding what the broker actually writes — and what the consumer actually reads — the guarantee is easy to break silently.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-a-transaction-actually-writes-to-a-partition&quot;&gt;What a Transaction Actually Writes to a Partition&lt;&#x2F;h3&gt;
&lt;p&gt;When a producer sends messages inside a transaction, Kafka does &lt;strong&gt;not&lt;&#x2F;strong&gt; immediately make those messages visible to consumers. The broker writes them to the partition log but marks them as part of an open transaction. Two types of special records are involved:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1. Regular data records&lt;&#x2F;strong&gt; — written with the producer&#x27;s &lt;code&gt;PID&lt;&#x2F;code&gt; (Producer ID) and a transaction flag set in the record batch header. They sit in the log at their assigned offsets, but are invisible to &lt;code&gt;read_committed&lt;&#x2F;code&gt; consumers until the transaction commits.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2. Transaction markers (control records)&lt;&#x2F;strong&gt; — written by the broker after the producer calls &lt;code&gt;commitTransaction()&lt;&#x2F;code&gt; or &lt;code&gt;abortTransaction()&lt;&#x2F;code&gt;. A marker is a special log entry at a new offset that signals &lt;code&gt;COMMIT&lt;&#x2F;code&gt; or &lt;code&gt;ABORT&lt;&#x2F;code&gt; for a given &lt;code&gt;PID&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Partition log (offsets):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;offset 0  │ data record  (PID=42, txn=open)   ← written during transaction&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;offset 1  │ data record  (PID=42, txn=open)   ← written during transaction&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;offset 2  │ COMMIT marker (PID=42)             ← written by broker on commitTransaction()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Without the &lt;code&gt;COMMIT&lt;&#x2F;code&gt; marker at offset 2, offsets 0 and 1 are not considered committed data — they are in limbo. An &lt;code&gt;ABORT&lt;&#x2F;code&gt; marker instead would tell consumers to discard offsets 0 and 1 entirely.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-transaction-coordinator-and-transaction-state&quot;&gt;The Transaction Coordinator and &lt;code&gt;__transaction_state&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Kafka has a dedicated internal component called the &lt;strong&gt;Transaction Coordinator&lt;&#x2F;strong&gt; — one per broker, selected by hashing the producer&#x27;s &lt;code&gt;transactional.id&lt;&#x2F;code&gt;. It manages transaction lifecycle and persists state in the internal topic &lt;strong&gt;&lt;code&gt;__transaction_state&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Transaction flow step by step:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Producer                        Transaction Coordinator           Partition Leaders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   │                                     │                              │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   ├─ initTransactions() ───────────────►│ assign PID + epoch           │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   │◄─ PID, epoch ───────────────────────┤                              │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   │                                     │                              │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   ├─ beginTransaction() [local only]    │                              │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   │                                     │                              │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   ├─ send(records) ─────────────────────┼──────────────────────────────► written, txn=open&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   │                                     │                              │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   ├─ sendOffsetsToTransaction() ───────►│ record consumed offsets      │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   │                                     │  in __transaction_state      │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   │                                     │                              │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   ├─ commitTransaction() ──────────────►│ write COMMIT to              │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   │                                     │  __transaction_state         │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   │                                     ├─ write COMMIT marker ────────► offset 2: COMMIT(PID=42)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   │◄─ ack ──────────────────────────────┤                              │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;code&gt;epoch&lt;&#x2F;code&gt; prevents zombie producers: if a producer crashes and restarts with the same &lt;code&gt;transactional.id&lt;&#x2F;code&gt;, it gets a new higher epoch. The old epoch is fenced — any in-flight batches from the old instance are rejected by brokers.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;isolation-level-read-committed-what-it-does&quot;&gt;&lt;code&gt;isolation.level=read_committed&lt;&#x2F;code&gt; — What It Does&lt;&#x2F;h3&gt;
&lt;p&gt;This is a &lt;strong&gt;consumer-side config&lt;&#x2F;strong&gt;. It controls which records from the partition log the consumer is allowed to see.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;read_uncommitted&lt;&#x2F;code&gt; (default)&lt;&#x2F;strong&gt; — the consumer reads every record at every offset as soon as it lands in the log, regardless of transaction state. It sees records from committed transactions, in-flight (open) transactions, and aborted transactions alike.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;read_committed&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — the consumer reads only records that are part of a committed transaction (or records written outside of any transaction). It does this by tracking the &lt;strong&gt;Last Stable Offset (LSO)&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;LSO = the offset up to which all transactions are resolved (committed or aborted)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Partition log:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;offset 0  │ data (PID=42, txn=open)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;offset 1  │ data (PID=42, txn=open)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;offset 2  │ COMMIT marker (PID=42)   ← LSO advances past here once this is written&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;offset 3  │ data (PID=99, txn=open)  ← LSO stops here; txn still open&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;offset 4  │ data (PID=99, txn=open)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A &lt;code&gt;read_committed&lt;&#x2F;code&gt; consumer can consume up to and including offset 2 (the LSO). Offsets 3 and 4 are held back until PID=99&#x27;s transaction resolves. Additionally, the broker sends an &lt;strong&gt;abort index&lt;&#x2F;strong&gt; alongside the fetch response — the consumer uses this to silently skip over records that belong to aborted transactions.&lt;&#x2F;p&gt;
&lt;p&gt;The consumer never sees the transaction markers themselves — those are internal control records filtered out by the client library.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-read-uncommitted-makes-transactions-useless&quot;&gt;Why &lt;code&gt;read_uncommitted&lt;&#x2F;code&gt; Makes Transactions Useless&lt;&#x2F;h3&gt;
&lt;p&gt;Suppose a producer reads from topic A, transforms the records, and writes to topic B — all inside a transaction. The goal is: either all writes to B succeed atomically, or none are visible.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;With &lt;code&gt;read_uncommitted&lt;&#x2F;code&gt; on the consumer of topic B:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;t=0  Producer begins transaction&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;t=1  Producer writes record-1 to topic B  (offset 0, txn=open)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;t=2  Producer writes record-2 to topic B  (offset 1, txn=open)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;t=3  Consumer of B reads offset 0 → sees record-1   ← PROBLEM&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;t=4  Producer crashes before commitTransaction()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;t=5  Transaction Coordinator writes ABORT marker&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;t=6  Consumer of B never reads offset 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The consumer already processed record-1 at t=3 — before the transaction was resolved. The producer aborted, so record-1 was never meant to be visible. But the &lt;code&gt;read_uncommitted&lt;&#x2F;code&gt; consumer has no way to know that — it already committed the offset and acted on the data. &lt;strong&gt;The atomicity guarantee is completely broken.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;With &lt;code&gt;read_committed&lt;&#x2F;code&gt;, the consumer at t=3 checks the LSO — offsets 0 and 1 are behind an open transaction, so the LSO has not advanced. The consumer&#x27;s fetch returns nothing. At t=5, when the ABORT marker lands, the broker sends the abort index and the consumer skips both records. The partition appears empty — exactly the correct behaviour.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;exactly-once-consume-transform-produce&quot;&gt;Exactly-Once Consume-Transform-Produce&lt;&#x2F;h3&gt;
&lt;p&gt;The canonical EOS pattern in Kafka is:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[Source Topic] ──► Consumer (read_committed) ──► Transform ──► Producer (transactional) ──► [Sink Topic]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                                                                     │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                                                          sendOffsetsToTransaction()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                                                          (commits consumed offsets&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                                                           atomically with the produce)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;sendOffsetsToTransaction()&lt;&#x2F;code&gt; registers the consumer group&#x27;s offsets as part of the current transaction. When the transaction commits, &lt;strong&gt;both&lt;&#x2F;strong&gt; the output records in the sink topic &lt;strong&gt;and&lt;&#x2F;strong&gt; the input offsets in &lt;code&gt;__consumer_offsets&lt;&#x2F;code&gt; are committed atomically. If the transaction aborts, neither is visible. This prevents the double-processing that would happen if the produce succeeded but the offset commit failed (or vice versa).&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Component&lt;&#x2F;th&gt;&lt;th&gt;Config&lt;&#x2F;th&gt;&lt;th&gt;Effect&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Producer&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;transactional.id&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Enables transactional API; assigns stable PID + epoch&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Producer&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;enable.idempotence=true&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Automatically enabled with &lt;code&gt;transactional.id&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Consumer&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;isolation.level=read_committed&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Only sees records from committed transactions&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Consumer&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;sendOffsetsToTransaction()&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Ties offset commit to the current transaction&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Without &lt;code&gt;isolation.level=read_committed&lt;&#x2F;code&gt; on every consumer downstream of the transactional producer, the transaction boundary means nothing — consumers will read and act on data from transactions that may still abort.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;schema-schema-registry&quot;&gt;Schema (Schema Registry)&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Compatibility modes:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Mode&lt;&#x2F;th&gt;&lt;th&gt;Meaning&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Backward&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;New schema can read data written with the old schema. Upgrade &lt;strong&gt;consumers first&lt;&#x2F;strong&gt;.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Forward&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Old schema can read data written with the new schema. Upgrade &lt;strong&gt;producers first&lt;&#x2F;strong&gt;.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Full (Transitive)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Both backward and forward compatible.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;&lt;strong&gt;Field changes and compatibility:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Adding a field with a default → backward compatible.&lt;&#x2F;li&gt;
&lt;li&gt;Removing a field that had a default → forward compatible.&lt;&#x2F;li&gt;
&lt;li&gt;Adding a required field (no default) → breaking change.&lt;&#x2F;li&gt;
&lt;li&gt;Removing a required field → breaking change.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;avro-binary-serialization-and-payload-efficiency&quot;&gt;Avro — Binary Serialization and Payload Efficiency&lt;&#x2F;h2&gt;
&lt;p&gt;Avro is a binary serialization format designed by Apache. In Kafka it is the most common choice for encoding messages because it significantly reduces wire size compared to JSON or XML.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-avro-saves-payload-size&quot;&gt;Why Avro Saves Payload Size&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;1. Schema is stored separately — not in every message.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;With the Confluent Schema Registry, the schema is registered once and assigned an integer ID. Each Kafka message contains only a compact 5-byte header:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[0x00]  [schema-id: 4 bytes]  [binary payload...]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; magic     registry pointer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Field names, types, and structure are never repeated in the message itself.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2. No key-value overhead.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;JSON must embed field names as strings in every record:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;userId&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 12345&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;Alice&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;active&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Avro writes only the values in the schema-defined order, binary-encoded — no keys, no quotes, no braces, no colons.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;3. Compact binary encoding for values.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Integers&lt;&#x2F;strong&gt;: variable-length zig-zag encoding. Small integers (0–63) fit in 1 byte. The number &lt;code&gt;12345&lt;&#x2F;code&gt; encodes to 3 bytes vs. 5 characters in JSON.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Strings&lt;&#x2F;strong&gt;: length-prefixed (varint length + raw UTF-8 bytes) — no surrounding quotes.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Booleans&lt;&#x2F;strong&gt;: 1 byte.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Null&lt;&#x2F;strong&gt;: 0 bytes in a union — nothing on the wire.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;4. Concrete size comparison&lt;&#x2F;strong&gt; — a simple record: &lt;code&gt;userId=1&lt;&#x2F;code&gt;, &lt;code&gt;name=&quot;Alice&quot;&lt;&#x2F;code&gt;, &lt;code&gt;active=true&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Format&lt;&#x2F;th&gt;&lt;th&gt;Encoded size (approx.)&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;JSON&lt;&#x2F;td&gt;&lt;td&gt;~40 bytes&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Avro binary (5-byte header + payload)&lt;&#x2F;td&gt;&lt;td&gt;~13 bytes&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Roughly 60–70% smaller in practice for typical domain objects. Savings grow larger as the number of fields increases, because JSON repeats every field name while Avro never does.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;avro-schema-example&quot;&gt;Avro Schema Example&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;  &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;record&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;  &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;User&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;  &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;namespace&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;com.example&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;  &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;fields&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;userId&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;int&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;},&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;   &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;string&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;},&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;active&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FE;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;boolean&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;how-the-schema-registry-fits-in&quot;&gt;How the Schema Registry Fits In&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Producer&lt;&#x2F;strong&gt; serializes a record using the Avro schema → checks&#x2F;registers the schema in the Registry → receives a schema ID → prepends the 5-byte header → sends to Kafka.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Consumer&lt;&#x2F;strong&gt; reads a message → reads the schema ID from the header → fetches the schema from the Registry (cached after first fetch) → deserializes the binary payload.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The schema is fetched &lt;strong&gt;once per unique schema ID&lt;&#x2F;strong&gt; and cached locally — subsequent deserialization is a pure in-memory lookup.&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
