<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Hands On Kafka : Kafka Mastery: Building StreamSocial - Java & Spring Boot Edition]]></title><description><![CDATA[Event-driven systems are one of the most deceptively difficult problems in backend engineering. Every non-trivial platform eventually needs to move data between services in real time, fan events out to dozens of downstream consumers, or guarantee that a payment event is processed exactly once — and the moment a system moves past a single-broker tutorial setup, the simple producer-and-consumer pattern that worked in a five-minute quickstart starts causing duplicate processing, out-of-order writes, and silent data loss in production.]]></description><link>https://handsonkafka.substack.com/s/kafka-mastery-building-streamsocial</link><image><url>https://substackcdn.com/image/fetch/$s_!D20Y!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b39b4b3-35a4-46ab-afc7-d57158f8d6cd_1024x1024.png</url><title>Hands On Kafka : Kafka Mastery: Building StreamSocial - Java &amp; Spring Boot Edition</title><link>https://handsonkafka.substack.com/s/kafka-mastery-building-streamsocial</link></image><generator>Substack</generator><lastBuildDate>Wed, 19 Aug 2026 22:07:06 GMT</lastBuildDate><atom:link href="https://handsonkafka.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[SystemDR]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[handsonkafka@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[handsonkafka@substack.com]]></itunes:email><itunes:name><![CDATA[Kafka]]></itunes:name></itunes:owner><itunes:author><![CDATA[Kafka]]></itunes:author><googleplay:owner><![CDATA[handsonkafka@substack.com]]></googleplay:owner><googleplay:email><![CDATA[handsonkafka@substack.com]]></googleplay:email><googleplay:author><![CDATA[Kafka]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Day 5: Engagement Consumer Development]]></title><description><![CDATA[What we&#8217;ll build today]]></description><link>https://handsonkafka.substack.com/p/day-5-engagement-consumer-development-624</link><guid isPermaLink="false">https://handsonkafka.substack.com/p/day-5-engagement-consumer-development-624</guid><dc:creator><![CDATA[Kafka]]></dc:creator><pubDate>Tue, 18 Aug 2026 08:31:07 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!4ms8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0ec3e3d-6ef8-41bc-8c54-d22f0892572b_1125x600.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>What we&#8217;ll build today</h2><blockquote><ul><li><p><code>streamsocial-engagement-consumer</code>: the first consumer, reading <code>content-interactions</code> (Day 3) for likes, comments, shares</p></li><li><p><code>@KafkaListener</code> with Spring&#8217;s <code>JsonDeserializer&lt;ContentInteractionEvent&gt;</code>, symmetric with Day 4&#8217;s producer-side serialization choices</p></li><li><p>A second dashboard panel, live, alongside the first &#8212; nothing about Day 4&#8217;s panel changes</p></li></ul></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4ms8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0ec3e3d-6ef8-41bc-8c54-d22f0892572b_1125x600.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4ms8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0ec3e3d-6ef8-41bc-8c54-d22f0892572b_1125x600.png 424w, https://substackcdn.com/image/fetch/$s_!4ms8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0ec3e3d-6ef8-41bc-8c54-d22f0892572b_1125x600.png 848w, https://substackcdn.com/image/fetch/$s_!4ms8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0ec3e3d-6ef8-41bc-8c54-d22f0892572b_1125x600.png 1272w, https://substackcdn.com/image/fetch/$s_!4ms8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0ec3e3d-6ef8-41bc-8c54-d22f0892572b_1125x600.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4ms8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0ec3e3d-6ef8-41bc-8c54-d22f0892572b_1125x600.png" width="502" height="267.73333333333335" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b0ec3e3d-6ef8-41bc-8c54-d22f0892572b_1125x600.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:600,&quot;width&quot;:1125,&quot;resizeWidth&quot;:502,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4ms8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0ec3e3d-6ef8-41bc-8c54-d22f0892572b_1125x600.png 424w, https://substackcdn.com/image/fetch/$s_!4ms8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0ec3e3d-6ef8-41bc-8c54-d22f0892572b_1125x600.png 848w, https://substackcdn.com/image/fetch/$s_!4ms8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0ec3e3d-6ef8-41bc-8c54-d22f0892572b_1125x600.png 1272w, https://substackcdn.com/image/fetch/$s_!4ms8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0ec3e3d-6ef8-41bc-8c54-d22f0892572b_1125x600.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>A correction worth making out loud</h2><p>The error-handling class this lesson was originally scoped around, <code>SeekToCurrentErrorHandler</code>, doesn&#8217;t exist in current Spring Kafka. It was deprecated in version 2.8 and removed outright in 3.x. That&#8217;s the kind of thing that happens constantly in a fast-moving ecosystem, and pretending otherwise would mean shipping a lesson that doesn&#8217;t compile against anything you&#8217;d actually install today. The replacement, <code>DefaultErrorHandler</code>, does the same job the deprecated class did: on a listener failure, it seeks the failed record &#8212; and everything after it in that poll batch &#8212; back to be redelivered by the broker, retries against a configurable backoff, and only after retries are exhausted hands the record to a recoverer instead of retrying forever or crashing the consumer thread.</p><h2>The poll loop, and what &#8220;consuming&#8221; actually means</h2><p>A Kafka consumer doesn&#8217;t get pushed messages &#8212; it polls. <code>poll()</code> asks the broker for the next batch of records sitting after the consumer&#8217;s current offset on whatever partitions it&#8217;s assigned, gets a batch back, and the application processes that batch before calling <code>poll()</code> again. <code>@KafkaListener</code> hides this loop entirely: Spring&#8217;s listener container runs it on a background thread and calls your annotated method once per record. What you&#8217;re writing is the body of that loop, not the loop itself.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://handsonkafka.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hands On Kafka  is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8A9k!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb499e6e2-bcc3-4944-bc84-7d03771bc71c_1125x1100.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8A9k!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb499e6e2-bcc3-4944-bc84-7d03771bc71c_1125x1100.png 424w, https://substackcdn.com/image/fetch/$s_!8A9k!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb499e6e2-bcc3-4944-bc84-7d03771bc71c_1125x1100.png 848w, https://substackcdn.com/image/fetch/$s_!8A9k!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb499e6e2-bcc3-4944-bc84-7d03771bc71c_1125x1100.png 1272w, https://substackcdn.com/image/fetch/$s_!8A9k!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb499e6e2-bcc3-4944-bc84-7d03771bc71c_1125x1100.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8A9k!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb499e6e2-bcc3-4944-bc84-7d03771bc71c_1125x1100.png" width="460" height="449.77777777777777" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b499e6e2-bcc3-4944-bc84-7d03771bc71c_1125x1100.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1100,&quot;width&quot;:1125,&quot;resizeWidth&quot;:460,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8A9k!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb499e6e2-bcc3-4944-bc84-7d03771bc71c_1125x1100.png 424w, https://substackcdn.com/image/fetch/$s_!8A9k!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb499e6e2-bcc3-4944-bc84-7d03771bc71c_1125x1100.png 848w, https://substackcdn.com/image/fetch/$s_!8A9k!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb499e6e2-bcc3-4944-bc84-7d03771bc71c_1125x1100.png 1272w, https://substackcdn.com/image/fetch/$s_!8A9k!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb499e6e2-bcc3-4944-bc84-7d03771bc71c_1125x1100.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Deserialization is where the producer&#8217;s choices come back</h2><p>Day 4&#8217;s producer configured <code>JsonSerializer.ADD_TYPE_INFO_HEADERS = false</code> &#8212; no type metadata riding along on every record, because every message on a given topic is one known type and saying so on every single message is waste. That decision has a mirror-image requirement on the consuming side: <code>JsonDeserializer</code> needs to be told the target type directly, <code>new JsonDeserializer&lt;&gt;(ContentInteractionEvent.class, false)</code>, rather than expecting a header that was never sent. Producer and consumer configuration aren&#8217;t independent &#8212; a serialization decision made in one module is a contract the other module has to honor, and getting that pairing wrong is a silent, confusing failure mode (deserialization exceptions with no obvious cause) rather than a loud one. Today&#8217;s own <code>EngagementTestDataGenerator</code> follows the exact same wire format for this reason &#8212; it&#8217;s standing in for a real <code>content-interactions</code> producer this course hasn&#8217;t built yet, and doing so faithfully matters.</p><h2>Validation didn&#8217;t stop mattering just because the JSON parsed</h2><p>Successfully deserializing a record proves the bytes were well-formed JSON matching the expected shape. It proves nothing about whether the <em>values</em> make sense. This listener runs every deserialized <code>ContentInteractionEvent</code> through the same <code>jakarta.validation.Validator</code> Day 1 used &#8212; a blank <code>contentId</code> that somehow made it onto the topic is caught here, thrown as an exception, and handled by the same retry-then-recover path as any other processing failure. Bean Validation isn&#8217;t a REST-layer-only concern; it&#8217;s useful anywhere a payload crosses a trust boundary, and a Kafka topic populated by services you don&#8217;t fully control is exactly that.</p><h2>What &#8220;graceful&#8221; actually means here</h2><p>Today&#8217;s failure hook &#8212; a deliberately poisoned <code>contentId</code> of <code>post-BOOM</code> &#8212; exists to make the failure path observable, not because that&#8217;s a real production scenario. What matters is what happens when it fires: the record gets retried twice, one second apart, and if it still fails, <code>RecoveryTracker</code> logs it as <code>STRUCTURED_ERROR</code> and counts it &#8212; instead of the consumer thread dying (which would stop every other partition assigned to it from being processed too) or the record being silently dropped. &#8220;Graceful&#8221; here specifically means: bounded retries, then a loud, greppable record of what got given up on. It does not yet mean routing the record somewhere it can be reprocessed later &#8212; that&#8217;s a dead letter topic, and that&#8217;s a later lesson&#8217;s job, once poison-pill handling gets the dedicated treatment it deserves.</p><h2>The dashboard gets its second panel</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!XxFk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ca1baf5-faf2-45cf-8a59-8463a591f80f_1125x575.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XxFk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ca1baf5-faf2-45cf-8a59-8463a591f80f_1125x575.png 424w, https://substackcdn.com/image/fetch/$s_!XxFk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ca1baf5-faf2-45cf-8a59-8463a591f80f_1125x575.png 848w, https://substackcdn.com/image/fetch/$s_!XxFk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ca1baf5-faf2-45cf-8a59-8463a591f80f_1125x575.png 1272w, https://substackcdn.com/image/fetch/$s_!XxFk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ca1baf5-faf2-45cf-8a59-8463a591f80f_1125x575.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!XxFk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ca1baf5-faf2-45cf-8a59-8463a591f80f_1125x575.png" width="504" height="257.6" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3ca1baf5-faf2-45cf-8a59-8463a591f80f_1125x575.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:575,&quot;width&quot;:1125,&quot;resizeWidth&quot;:504,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!XxFk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ca1baf5-faf2-45cf-8a59-8463a591f80f_1125x575.png 424w, https://substackcdn.com/image/fetch/$s_!XxFk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ca1baf5-faf2-45cf-8a59-8463a591f80f_1125x575.png 848w, https://substackcdn.com/image/fetch/$s_!XxFk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ca1baf5-faf2-45cf-8a59-8463a591f80f_1125x575.png 1272w, https://substackcdn.com/image/fetch/$s_!XxFk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3ca1baf5-faf2-45cf-8a59-8463a591f80f_1125x575.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>ContentInteractionsFeedListener</code> follows the exact shape <code>UserActionsFeedListener</code> established Day 4 &#8212; its own consumer group (<code>dashboard-live-feed-content-interactions</code>, deliberately distinct from both <code>engagement-consumer</code>&#8216;s group and the <code>user-actions</code> panel&#8217;s own group), broadcasting through the same <code>EventStreamBroadcaster</code> to a second SSE stream. Nothing about the <code>user-actions</code> panel changes; the two feeds run side by side, each backed by its own independent, observational consumer. Run <code>EngagementTestDataGenerator</code> and the same 14 events show up in two places at once &#8212; <code>streamsocial-engagement-consumer</code>&#8216;s own log (processing them for real, with validation and error handling) and the dashboard&#8217;s browser view (just watching, no business logic at all) &#8212; which is itself a small, live demonstration of what a consumer group actually buys you: two entirely independent readers of the same topic, neither aware of the other, both correct.</p><h1>Implementation Guide: </h1><h2>GitHub Link :</h2><p><a href="https://github.com/sysdr/streamsocial-java/tree/main/day05-v2-streamsocial-source/streamsocial">https://github.com/sysdr/streamsocial-java/tree/main/day05-v2-streamsocial-source/streamsocial</a></p><h2>Build, Test, and Demo the Engagement Consumer</h2><h2>The idea, in pseudo-code</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;17c8dd72-3ef3-4044-a4a3-13655914f50d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">KafkaConsumerConfig:
    consumerFactory = DefaultKafkaConsumerFactory(
        bootstrap.servers, group.id="engagement-consumer",
        keyDeserializer=String, valueDeserializer=Json(ContentInteractionEvent))

    errorHandler = DefaultErrorHandler(
        recoverer = RecoveryTracker::recordRecovery,
        backOff = FixedBackOff(1000ms, retries=2))
    # replaces the removed SeekToCurrentErrorHandler - same seek-and-retry job

@KafkaListener("content-interactions"):
    onInteraction(record):
        event = record.value()
        if not valid(event): throw   # -&gt; retried, then recovered
        if event.contentId == "post-BOOM": throw  # today's demo hook
        log STRUCTURED_EVENT ...
        processed.add(event)

# Dashboard, same shape as Day 4's panel, own group:
ContentInteractionsFeedListener:
    group.id = "dashboard-live-feed-content-interactions"
    on record: broadcast LiveFeedItem to SSE subscribers of "content-interactions"
</code></pre></div><h2>Step 1 &#8212; Bring up the cluster and topics</h2><pre><code><code>cd streamsocial-infra &amp;&amp; ./scripts/start.sh &amp;&amp; cd ..
mvn org.codehaus.mojo:exec-maven-plugin:3.3.0:java -pl streamsocial-common \
  -Dexec.mainClass=com.streamsocial.common.demo.TopicBootstrapDemo \
  -Dstreamsocial.topics.user-actions-partitions=12 \
  -Dstreamsocial.topics.content-interactions-partitions=6</code></code></pre><h2>Step 2 &#8212; Start the dashboard and the consumer</h2><pre><code><code>mvn -pl streamsocial-dashboard -am spring-boot:run</code></code></pre><p>Open http://localhost:8080 &#8212; two panels now, <code>user-actions</code> and <code>content-interactions</code>, both at 0.</p><p>In another terminal:</p><pre><code><code>mvn -pl streamsocial-engagement-consumer -am spring-boot:run</code></code></pre><p><strong>Expected:</strong> Spring Boot startup log, ending quietly &#8212; no web server line, since this module has none.</p><h2>Step 3 &#8212; Generate test traffic and watch both places at once</h2><pre><code><code>mvn org.codehaus.mojo:exec-maven-plugin:3.3.0:java -pl streamsocial-engagement-consumer \
  -Dexec.mainClass=com.streamsocial.consumer.demo.EngagementTestDataGenerator</code></code></pre><p><strong>Expected in the consumer&#8217;s terminal:</strong> 14 lines like:</p><pre><code><code>STRUCTURED_EVENT event=engagement-processed interactionType=CONTENT_LIKED userId=user-0 contentId=post-0 partition=1 offset=0</code></code></pre><p>Then, a couple seconds later:</p><pre><code><code>STRUCTURED_ERROR event=engagement-processing-recovered topic=content-interactions partition=... offset=... key=post-BOOM reason=simulated processing failure for post-BOOM</code></code></pre><p><strong>Expected on the dashboard, at roughly the same time:</strong> the <code>content-interactions</code> panel&#8217;s counter climbs to 14 (not 15 &#8212; the poisoned event never succeeds, so it never gets broadcast either, the same way it never lands in the consumer&#8217;s <code>processed</code> list) and 14 rows appear.</p><h2>Step 4 &#8212; Run the tests</h2><pre><code><code>mvn -pl streamsocial-engagement-consumer -am verify
mvn -pl streamsocial-dashboard -am verify</code></code></pre><p><strong>Expected:</strong> all green. <code>EngagementEventListenerIT</code>&#8216;s <code>poisonedEventIsRetriedThenRecoveredNotLostSilently</code> takes noticeably longer than the other tests &#8212; it&#8217;s waiting out the real retry backoff, not asserting instantly. <code>ContentInteractionsFeedBroadcastIT</code> proves the new panel&#8217;s data path the same way Day 4&#8217;s <code>UserActionsFeedBroadcastIT</code> did for the first one.</p><h2>Success criteria </h2><p><code>EngagementEventListenerIT</code> passes &#8212; both the normal-processing case and the poisoned-record recovery case, against a real broker, with the real listener bean. Run the test-data generator against a live consumer and dashboard together, and find <code>STRUCTURED_EVENT</code>/<code>STRUCTURED_ERROR</code> lines in the consumer&#8217;s log at the same moment matching rows appear on the dashboard&#8217;s second panel.</p><h2>Working Demo Link :</h2><div id="youtube2-XK7NJGdx0OA" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;XK7NJGdx0OA&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/XK7NJGdx0OA?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><h2>Homework</h2><p>Add a second <code>@KafkaListener</code> method to a new class, <code>EngagementMetricsListener</code>, that listens to <code>content-interactions</code> but with a <strong>different</strong> <code>group.id</code>. Have it maintain a simple in-memory count per <code>InteractionType</code>.</p><p>Implementation checklist:</p><ol><li><p>New listener class, new <code>@KafkaListener(topics = "content-interactions", groupId = "engagement-metrics")</code> with its own <code>ConsumerFactory</code>/container factory bean, following <code>KafkaConsumerConfig</code>&#8216;s existing pattern.</p></li><li><p>A <code>ConcurrentHashMap&lt;InteractionType, Long&gt;</code> (or similar) tallying counts, with a getter for tests.</p></li><li><p>A test that publishes a known mix of interaction types and asserts the final counts match.</p></li><li><p>Run both listeners at once and confirm <strong>both</strong> process every message &#8212; think about why a different <code>group.id</code> makes that true, tying back to what today&#8217;s dashboard panels already demonstrated.</p></li></ol><h2>Solution hints</h2><ul><li><p>Two consumers in two different groups both get their own copy of every record &#8212; consumer groups are what make records get <em>split</em> across consumers, not consumers in different groups compete for the same records. The dashboard&#8217;s two independent panels reading the same topics as the business consumers are already a live example of exactly this.</p></li><li><p>Your metrics listener doesn&#8217;t need the <code>DefaultErrorHandler</code>/retry setup this lesson&#8217;s main listener has &#8212; it&#8217;s fine for homework purposes to let a bad record just log and move on, since the goal here is understanding group isolation, not re-solving today&#8217;s error handling.</p></li><li><p>If your counts come out lower than expected, double-check you created a <strong>new</strong>, distinctly-named container factory bean for the new group ID rather than accidentally reusing <code>engagementListenerContainerFactory</code>, which is already bound to <code>group.id=engagement-consumer</code> at the factory level.</p></li></ul><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://handsonkafka.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hands On Kafka  is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Day 4: High-Volume Producer Implementation]]></title><description><![CDATA[What we&#8217;re building today]]></description><link>https://handsonkafka.substack.com/p/day-4-high-volume-producer-implementation-719</link><guid isPermaLink="false">https://handsonkafka.substack.com/p/day-4-high-volume-producer-implementation-719</guid><dc:creator><![CDATA[Kafka]]></dc:creator><pubDate>Sat, 15 Aug 2026 08:31:23 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!hQlE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7746c87-b1db-4220-89eb-cb617f6207bc_1025x575.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>What we&#8217;re building today</h2><blockquote><p><code>streamsocial-producer-service</code> &#8212; a Spring Boot app exposing <code>POST /api/posts</code>, backed by a <strong>pooled</strong> <code>KafkaTemplate&lt;String, UserActionEvent&gt;</code></p><p><code>streamsocial-dashboard</code> &#8212; the system&#8217;s first browser-visible surface: a live events/sec counter fed by a raw <code>kafka-clients</code> <code>Consumer</code>, pushed to the page over Server-Sent Events</p><p>The moment where Day 1&#8217;s event records and Day 3&#8217;s topics stop being things you can only prove with a CLI, and start being things you can watch happen</p><p>This is the first lesson where StreamSocial does something a browser can show you. Everything before today built the plumbing; today the plumbing carries real water.</p></blockquote><h2>Core concept: a producer is thread-safe, but &#8220;thread-safe&#8221; isn&#8217;t &#8220;fast enough&#8221;</h2><blockquote><p>A single <code>KafkaProducer</code> can be shared safely across every thread in your app &#8212; it&#8217;s built for that. But under real concurrent load, that one producer&#8217;s internal network client becomes a queue everyone waits behind. <code>DefaultKafkaProducerFactory.setProducerPerThread(true)</code> gives each calling thread its own producer instead, pulled from a <code>ThreadLocal</code> pool &#8212; that&#8217;s what &#8220;connection pooling&#8221; means for a Kafka producer, distinct from the JDBC sense of the term most engineers learn first.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hQlE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7746c87-b1db-4220-89eb-cb617f6207bc_1025x575.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hQlE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7746c87-b1db-4220-89eb-cb617f6207bc_1025x575.png 424w, https://substackcdn.com/image/fetch/$s_!hQlE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7746c87-b1db-4220-89eb-cb617f6207bc_1025x575.png 848w, https://substackcdn.com/image/fetch/$s_!hQlE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7746c87-b1db-4220-89eb-cb617f6207bc_1025x575.png 1272w, https://substackcdn.com/image/fetch/$s_!hQlE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7746c87-b1db-4220-89eb-cb617f6207bc_1025x575.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hQlE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7746c87-b1db-4220-89eb-cb617f6207bc_1025x575.png" width="506" height="283.8536585365854" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a7746c87-b1db-4220-89eb-cb617f6207bc_1025x575.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:575,&quot;width&quot;:1025,&quot;resizeWidth&quot;:506,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!hQlE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7746c87-b1db-4220-89eb-cb617f6207bc_1025x575.png 424w, https://substackcdn.com/image/fetch/$s_!hQlE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7746c87-b1db-4220-89eb-cb617f6207bc_1025x575.png 848w, https://substackcdn.com/image/fetch/$s_!hQlE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7746c87-b1db-4220-89eb-cb617f6207bc_1025x575.png 1272w, https://substackcdn.com/image/fetch/$s_!hQlE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7746c87-b1db-4220-89eb-cb617f6207bc_1025x575.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><blockquote><p>The other half of today&#8217;s concept is what key you send with. <code>PostProducerService</code> keys every <code>PostCreated</code> event by <code>userId</code>, not randomly or round-robin &#8212; because Kafka guarantees ordering only <em>within</em> a partition, and every event for the same user has to land on the same partition to keep that user&#8217;s timeline in order. You&#8217;ll go deep on this on Day 14; today, just notice it&#8217;s already there in the code, on purpose.</p></blockquote><h2>Where this fits in StreamSocial</h2><blockquote><p>The producer service is the front door: it&#8217;s the only thing standing between &#8220;a user tapped Post&#8221; and a durable fact in Kafka. Everything downstream &#8212; Day 5&#8217;s engagement consumer, Day 41&#8217;s Streams topology, Day 44&#8217;s trending scores &#8212; depends on this door working correctly and staying open under load, because none of them can process an event that never arrived.</p><p>The dashboard, meanwhile, isn&#8217;t a side project &#8212; it&#8217;s the same kind of real-time visibility system StreamSocial&#8217;s own engineers would build to answer &#8220;is ingestion healthy right now?&#8221; without SSH-ing into a broker. It reads <code>user-actions</code> with a plain <code>KafkaConsumer</code> on a fresh, random <code>group.id</code> every time it starts, subscribed from <code>latest</code> &#8212; a monitoring consumer has no business replaying history, it only cares what&#8217;s happening <em>now</em>.</p></blockquote><h2>Architecture: request in, fact out, dashboard watching</h2><p>A <code>POST /api/posts</code> becomes a <code>PostCreated</code> event, gets validated by the same Bean Validation constraints Day 1 defined, and is handed to the pooled <code>KafkaTemplate</code>. The send is asynchronous &#8212; the HTTP response doesn&#8217;t block on the broker round-trip, it completes when Kafka acknowledges the write and returns the real partition and offset the event landed on.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tFmF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1afd3bfb-b2ca-444b-9dfe-031b447d47f2_1025x350.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tFmF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1afd3bfb-b2ca-444b-9dfe-031b447d47f2_1025x350.png 424w, https://substackcdn.com/image/fetch/$s_!tFmF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1afd3bfb-b2ca-444b-9dfe-031b447d47f2_1025x350.png 848w, https://substackcdn.com/image/fetch/$s_!tFmF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1afd3bfb-b2ca-444b-9dfe-031b447d47f2_1025x350.png 1272w, https://substackcdn.com/image/fetch/$s_!tFmF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1afd3bfb-b2ca-444b-9dfe-031b447d47f2_1025x350.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tFmF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1afd3bfb-b2ca-444b-9dfe-031b447d47f2_1025x350.png" width="520" height="177.5609756097561" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1afd3bfb-b2ca-444b-9dfe-031b447d47f2_1025x350.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:350,&quot;width&quot;:1025,&quot;resizeWidth&quot;:520,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tFmF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1afd3bfb-b2ca-444b-9dfe-031b447d47f2_1025x350.png 424w, https://substackcdn.com/image/fetch/$s_!tFmF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1afd3bfb-b2ca-444b-9dfe-031b447d47f2_1025x350.png 848w, https://substackcdn.com/image/fetch/$s_!tFmF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1afd3bfb-b2ca-444b-9dfe-031b447d47f2_1025x350.png 1272w, https://substackcdn.com/image/fetch/$s_!tFmF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1afd3bfb-b2ca-444b-9dfe-031b447d47f2_1025x350.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The dashboard&#8217;s browser connection has its own lifecycle, independent of any single event: it opens an <code>EventSource</code>, sits in a live state receiving a JSON snapshot every second from the server&#8217;s <code>@Scheduled</code> broadcaster, and silently reconnects if the connection drops &#8212; the browser&#8217;s <code>EventSource</code> API does that retry for you, no extra code required.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://handsonkafka.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hands On Kafka  is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!d5Ha!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bccadb5-9afc-41a9-bbe2-41d7059008fe_1025x475.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!d5Ha!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bccadb5-9afc-41a9-bbe2-41d7059008fe_1025x475.png 424w, https://substackcdn.com/image/fetch/$s_!d5Ha!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bccadb5-9afc-41a9-bbe2-41d7059008fe_1025x475.png 848w, https://substackcdn.com/image/fetch/$s_!d5Ha!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bccadb5-9afc-41a9-bbe2-41d7059008fe_1025x475.png 1272w, https://substackcdn.com/image/fetch/$s_!d5Ha!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bccadb5-9afc-41a9-bbe2-41d7059008fe_1025x475.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!d5Ha!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bccadb5-9afc-41a9-bbe2-41d7059008fe_1025x475.png" width="502" height="232.6341463414634" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1bccadb5-9afc-41a9-bbe2-41d7059008fe_1025x475.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:475,&quot;width&quot;:1025,&quot;resizeWidth&quot;:502,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!d5Ha!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bccadb5-9afc-41a9-bbe2-41d7059008fe_1025x475.png 424w, https://substackcdn.com/image/fetch/$s_!d5Ha!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bccadb5-9afc-41a9-bbe2-41d7059008fe_1025x475.png 848w, https://substackcdn.com/image/fetch/$s_!d5Ha!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bccadb5-9afc-41a9-bbe2-41d7059008fe_1025x475.png 1272w, https://substackcdn.com/image/fetch/$s_!d5Ha!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bccadb5-9afc-41a9-bbe2-41d7059008fe_1025x475.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h2>Implementation Guide :</h2><h3>GitHub Link :</h3><p><a href="https://github.com/sysdr/streamsocial-java/tree/main/streamsocial-day04/streamsocial">https://github.com/sysdr/streamsocial-java/tree/main/streamsocial-day04/streamsocial</a></p><h2>Fundamentals first</h2><p><code>KafkaProducer</code> batches records internally and manages its own background I/O thread &#8212; that&#8217;s <em>why</em> it&#8217;s thread-safe to share. The pooling question isn&#8217;t &#8220;is one producer safe to share,&#8221; it&#8217;s &#8220;does sharing it become a bottleneck before your network or the broker does.&#8221; At real scale, the answer is often yes, which is why Day 4&#8217;s pattern exists at all.</p><h2>Today&#8217;s specific pattern: producer-per-thread + async send</h2><p>Pseudo-code shape of the pooling config:</p><pre><code><code>ProducerFactory&lt;String, UserActionEvent&gt; factory = new DefaultKafkaProducerFactory(props)
factory.setProducerPerThread(true)   // each thread gets its own pooled producer, not one shared instance
KafkaTemplate template = new KafkaTemplate(factory)</code></code></pre><p>And the send path itself, keyed and async:</p><pre><code><code>event = new PostCreated(eventId, now, userId, postId, content)
template.send(topic="user-actions", key=event.userId(), value=event)
    .thenApply(result -&gt; respond 202 with result.partition(), result.offset())</code></code></pre><p>Nothing here blocks the HTTP thread on the broker round-trip &#8212; the <code>CompletableFuture</code> completes when Kafka acknowledges the write, and the controller maps that into the HTTP response.</p><h2>How it snaps into the system</h2><p><code>streamsocial-producer-service</code> imports <code>PostCreated</code>/<code>UserActionEvent</code> from <code>streamsocial-common</code> (Day 1) &#8212; it does not redefine them. It sends to <code>user-actions</code>, the exact topic Day 3&#8217;s <code>TopicBootstrap</code> created &#8212; it does not create its own topic. <code>streamsocial-dashboard</code> reads that same topic independently, with its own consumer group, so producer and dashboard have zero direct coupling to each other &#8212; only to the topic between them.</p><h2>Build, test, and demo &#8212; step by step</h2><p><strong>1. Unit tests (no broker needed)</strong></p><pre><code><code>cd streamsocial
mvn -pl streamsocial-producer-service,streamsocial-dashboard -am test</code></code></pre><p>Expected: <code>CreatePostRequestValidationTest</code> (3 tests) and <code>ThroughputTrackerTest</code> (2 tests) pass &#8212; these test validation and the throughput-counting logic in isolation.</p><p><strong>2. Integration test (real broker via Testcontainers)</strong></p><pre><code><code>mvn -pl streamsocial-producer-service -am verify</code></code></pre><p>Expected: <code>PostProducerServiceIT</code> spins up a real Kafka container, sends a real event, and reads it back with a real consumer. On a standard Docker host or CI runner this passes outright. If your sandbox&#8217;s Docker bridge doesn&#8217;t expose a Testcontainers-compatible API (some WSL2 + Docker Desktop setups don&#8217;t), <code>start.sh</code> detects that specific failure and falls back to manual verification against the already-running cluster instead of halting &#8212; check its output for which path actually ran.</p><p><strong>3. Full lifecycle, with Docker</strong></p><pre><code><code>./start.sh</code></code></pre><p>Expected: cluster up, topics bootstrapped (12/6 partitions locally &#8212; production default is 1000/500, see below), both services healthy, 5 real posts sent, and a live SSE snapshot printed showing <code>totalEvents</code> reflecting them.</p><p><strong>4. Manual verification</strong></p><pre><code><code>curl -X POST localhost:8082/api/posts \
  -H "Content-Type: application/json" \
  -d '{"userId":"&lt;any-uuid&gt;","content":"hello StreamSocial"}'</code></code></pre><p>Expected: <code>202 Accepted</code> with a real <code>partition</code> and <code>offset</code>. </p><p>Open http://localhost:8080 and watch the events/sec number spike the instant you send another one.</p><p><strong>5. No-Docker note</strong></p><p>The producer and dashboard are plain Spring Boot apps &#8212; <code>mvn -pl streamsocial-producer-service spring-boot:run</code> works without Docker for compiling/starting the JVM process itself, but both need a real broker to do anything useful, so Docker (or a broker reachable some other way) is required for this lesson&#8217;s actual demo, unlike Day 1.</p><p><strong>6. Shut down</strong></p><pre><code><code>./stop.sh</code></code></pre><p>Expected: both apps and all three brokers stop cleanly; safe to re-run <code>start.sh</code> immediately after.</p><h2>The local-scale vs. production-scale gap</h2><p>The Day 4 challenge target is 5M posts/second. This lesson&#8217;s code doesn&#8217;t pretend to hit that on a laptop against a 3-broker local cluster &#8212; that number comes from partitioning (1000 partitions on <code>user-actions</code>, per Day 3) and running across a real horizontally-scaled cluster, not from any one producer&#8217;s configuration. What today&#8217;s code proves is the <em>mechanism</em> &#8212; pooling, async sends, correct keying &#8212; at a scale your machine can actually run in seconds.</p><h2>Success criterion</h2><p>Run <code>./start.sh</code>. It will bring up the 3-broker cluster, bootstrap topics, start both new services, fire 5 real posts through the producer, and print a live snapshot from the dashboard&#8217;s own SSE stream &#8212; you should see <code>totalEvents</code> in that snapshot match what was just posted. Then open http://localhost:8080 yourself, <code>curl -X POST localhost:8082/api/posts</code> a few more times by hand, and watch the number move in real time. If you can explain why the dashboard&#8217;s consumer never commits an offset, you&#8217;ve got today&#8217;s concept.</p><h2>Working Demo Link :</h2><div id="youtube2-_SYQB-hp6YY" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;_SYQB-hp6YY&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/_SYQB-hp6YY?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><h2>Homework assignment</h2><ol><li><p>Add a <code>PostProducerThroughputTest</code> (JUnit, real broker via Testcontainers) that sends 1,000 events in a tight loop across multiple threads and asserts it completes in under 5 seconds &#8212; a real, if modest, throughput floor.</p></li><li><p>Change the dashboard&#8217;s sparkline window from 30 seconds to 60, and confirm <code>ThroughputTrackerTest</code>&#8216;s history-cap test still passes after you update its expected size.</p></li><li><p>Add a second dashboard panel: total events <em>per partition</em> (not just total), using the same <code>ConsumerRecord.partition()</code> you already have access to in the poll loop.</p></li></ol><h2>Solution hints</h2><ul><li><p>For the throughput test, submit sends from an <code>ExecutorService</code> with several threads and <code>CompletableFuture.allOf(...).get()</code> to wait for all of them &#8212; this is exactly what exercises <code>setProducerPerThread(true)</code>.</p></li><li><p><code>ThroughputTracker</code>&#8216;s <code>HISTORY_SIZE</code> constant is the only thing that needs to change for the 60-second window; the deque logic already handles any size.</p></li><li><p>Per-partition counts need a <code>Map&lt;Integer, AtomicLong&gt;</code> inside <code>ThroughputTracker</code> rather than a single counter &#8212; keyed by <code>record.partition()</code> from the consumer, broadcast the same way as the existing snapshot.</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://handsonkafka.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hands On Kafka  is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Day 3: Topics & Partitions Strategy ]]></title><description><![CDATA[What we&#8217;ll build today]]></description><link>https://handsonkafka.substack.com/p/day-3-topics-and-partitions-strategy-069</link><guid isPermaLink="false">https://handsonkafka.substack.com/p/day-3-topics-and-partitions-strategy-069</guid><dc:creator><![CDATA[Kafka]]></dc:creator><pubDate>Mon, 10 Aug 2026 08:30:20 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!gOc0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F810a9a8d-59aa-4e8b-acca-06e44433c0cb_1125x687.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>What we&#8217;ll build today</h2><blockquote><ul><li><p><code>user-actions</code> and <code>content-interactions</code>, the first two topics on the Day 2 cluster</p></li><li><p><code>TopicBootstrapper</code>: an idempotent, <code>AdminClient</code>-based provisioning class in <code>streamsocial-common</code> &#8212; the first code in this repository that talks to Kafka at all</p></li><li><p>The actual math behind why one topic gets 1000 partitions and the other gets 500, not a round number picked because it looked reasonable</p></li></ul></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!gOc0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F810a9a8d-59aa-4e8b-acca-06e44433c0cb_1125x687.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gOc0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F810a9a8d-59aa-4e8b-acca-06e44433c0cb_1125x687.png 424w, https://substackcdn.com/image/fetch/$s_!gOc0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F810a9a8d-59aa-4e8b-acca-06e44433c0cb_1125x687.png 848w, https://substackcdn.com/image/fetch/$s_!gOc0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F810a9a8d-59aa-4e8b-acca-06e44433c0cb_1125x687.png 1272w, https://substackcdn.com/image/fetch/$s_!gOc0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F810a9a8d-59aa-4e8b-acca-06e44433c0cb_1125x687.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!gOc0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F810a9a8d-59aa-4e8b-acca-06e44433c0cb_1125x687.png" width="500" height="305.3333333333333" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/810a9a8d-59aa-4e8b-acca-06e44433c0cb_1125x687.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:687,&quot;width&quot;:1125,&quot;resizeWidth&quot;:500,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!gOc0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F810a9a8d-59aa-4e8b-acca-06e44433c0cb_1125x687.png 424w, https://substackcdn.com/image/fetch/$s_!gOc0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F810a9a8d-59aa-4e8b-acca-06e44433c0cb_1125x687.png 848w, https://substackcdn.com/image/fetch/$s_!gOc0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F810a9a8d-59aa-4e8b-acca-06e44433c0cb_1125x687.png 1272w, https://substackcdn.com/image/fetch/$s_!gOc0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F810a9a8d-59aa-4e8b-acca-06e44433c0cb_1125x687.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>A partition is the unit everything else scales around</h2><p>A Kafka topic isn&#8217;t one log &#8212; it&#8217;s however many partitions you give it, each an independently ordered, independently replicated append-only log. That single fact is why partition count is a decision made once, carefully, rather than a config value bumped later without thinking: partition count sets the ceiling on consumer parallelism (a partition can only be read by one consumer within a group at a time), and it&#8217;s the unit <code>min.insync.replicas</code> and replication actually operate on.</p><p>Get it too low and you cap your own future scale-out. Get it too high relative to what a topic&#8217;s key space can actually spread across, and you pay real costs &#8212; more file handles per broker, longer leader-election windows during failover, more replication traffic &#8212; for parallelism nothing ever uses.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://handsonkafka.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hands On Kafka  is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>The math, not a guess</h2><p>StreamSocial&#8217;s platform-wide target is 50M events/second at peak, across every topic. Today&#8217;s two topics carry an assumed split of that: 30M/sec on <code>user-actions</code> (posts, follows, profile updates), 20M/sec on <code>content-interactions</code> (likes, comments, shares).</p><p>Using a conservative sustained-throughput budget of roughly 30,000 events/sec per partition &#8212; small JSON events, replication factor 3, headroom left for consumer fan-out &#8212; the arithmetic is direct:</p><pre><code><code>user-actions:          30,000,000 / 30,000  &#8776; 1000 partitions
content-interactions:  20,000,000 / 30,000  &#8776;  667 partitions</code></code></pre><p><code>user-actions</code> lands on 1000 partitions, matching the math. <code>content-interactions</code> gets capped at <strong>500</strong> &#8212; deliberately below what the raw division suggests.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1vI_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394e5858-f941-4d92-980b-26ca168e19aa_1187x787.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1vI_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394e5858-f941-4d92-980b-26ca168e19aa_1187x787.png 424w, https://substackcdn.com/image/fetch/$s_!1vI_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394e5858-f941-4d92-980b-26ca168e19aa_1187x787.png 848w, https://substackcdn.com/image/fetch/$s_!1vI_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394e5858-f941-4d92-980b-26ca168e19aa_1187x787.png 1272w, https://substackcdn.com/image/fetch/$s_!1vI_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394e5858-f941-4d92-980b-26ca168e19aa_1187x787.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1vI_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394e5858-f941-4d92-980b-26ca168e19aa_1187x787.png" width="508" height="336.81213142375736" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/394e5858-f941-4d92-980b-26ca168e19aa_1187x787.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:787,&quot;width&quot;:1187,&quot;resizeWidth&quot;:508,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!1vI_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394e5858-f941-4d92-980b-26ca168e19aa_1187x787.png 424w, https://substackcdn.com/image/fetch/$s_!1vI_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394e5858-f941-4d92-980b-26ca168e19aa_1187x787.png 848w, https://substackcdn.com/image/fetch/$s_!1vI_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394e5858-f941-4d92-980b-26ca168e19aa_1187x787.png 1272w, https://substackcdn.com/image/fetch/$s_!1vI_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394e5858-f941-4d92-980b-26ca168e19aa_1187x787.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Why content-interactions is capped below the math</h2><p>This is the part worth sitting with, because it&#8217;s the opposite of &#8220;more partitions is always safer.&#8221; <code>user-actions</code> is keyed by <code>userId</code>: with millions of distinct users, traffic spreads close to evenly across however many partitions you give it, so raw partition count converts almost directly into usable parallelism.</p><p><code>content-interactions</code> is keyed by <code>contentId</code>, and content interaction volume follows a power law &#8212; a small number of posts go viral and absorb a wildly disproportionate share of likes, comments, and shares, while most content gets a handful of interactions total. Give that topic 667 or even 1000 partitions and the hot keys still concentrate on however many partitions their hashes land on; the rest of the extra partitions sit close to idle. You&#8217;ve added broker overhead &#8212; more replicated logs, more leader elections to manage on failover &#8212; without buying any real relief for the actual bottleneck. That bottleneck gets fixed in Day 15 with a smarter <code>Partitioner</code>, not with a bigger partition count today. Capping at 500 here is that engineering judgment made explicit, not a compromise.</p><h2>Idempotent, not &#8220;run once and hope&#8221;</h2><p><code>TopicBootstrapper</code> follows a specific, proven shape: list what exists, diff against the desired catalog, only create what&#8217;s actually missing. Running it twice in a row against the same cluster produces the same end state and doesn&#8217;t throw on the second run just because the topics already exist. It&#8217;s extended one step further than the minimal version: topics that already exist get their partition count <strong>verified</strong> against the catalog, not just assumed correct &#8212; Kafka can&#8217;t shrink partitions at all, and growing them later changes which partition a given key hashes to for every message already flowing through the topic, quietly breaking per-key ordering guarantees downstream consumers depend on. A mismatch is reported loudly, not silently fixed; that&#8217;s a decision for a human.</p><p>This is also the first code in the repository with a <code>kafka-clients</code> dependency &#8212; via <code>Admin</code>/<code>AdminClient</code> only. Nothing here produces or consumes a message; that&#8217;s Day 4 and Day 5&#8217;s job.</p><h2>Today&#8217;s demo runs at local-demo-scale &#8212; and says so</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6YBi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1bd9b9-0543-40f5-94af-c2a6be1bbb3b_1025x562.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6YBi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1bd9b9-0543-40f5-94af-c2a6be1bbb3b_1025x562.png 424w, https://substackcdn.com/image/fetch/$s_!6YBi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1bd9b9-0543-40f5-94af-c2a6be1bbb3b_1025x562.png 848w, https://substackcdn.com/image/fetch/$s_!6YBi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1bd9b9-0543-40f5-94af-c2a6be1bbb3b_1025x562.png 1272w, https://substackcdn.com/image/fetch/$s_!6YBi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1bd9b9-0543-40f5-94af-c2a6be1bbb3b_1025x562.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6YBi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1bd9b9-0543-40f5-94af-c2a6be1bbb3b_1025x562.png" width="504" height="276.3395121951219" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3a1bd9b9-0543-40f5-94af-c2a6be1bbb3b_1025x562.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:562,&quot;width&quot;:1025,&quot;resizeWidth&quot;:504,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6YBi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1bd9b9-0543-40f5-94af-c2a6be1bbb3b_1025x562.png 424w, https://substackcdn.com/image/fetch/$s_!6YBi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1bd9b9-0543-40f5-94af-c2a6be1bbb3b_1025x562.png 848w, https://substackcdn.com/image/fetch/$s_!6YBi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1bd9b9-0543-40f5-94af-c2a6be1bbb3b_1025x562.png 1272w, https://substackcdn.com/image/fetch/$s_!6YBi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1bd9b9-0543-40f5-94af-c2a6be1bbb3b_1025x562.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>StreamSocialTopics</code> carries <strong>1000</strong> and <strong>500</strong> as its real, documented production defaults &#8212; that&#8217;s what the code says, and what production would actually run. Standing up 1000 partitions on a single 3-broker laptop cluster works, but it&#8217;s needlessly slow for a five-minute check, so today&#8217;s automated demo overrides both counts via system property to <strong>12</strong> and <strong>6</strong> &#8212; the same 2:1 ratio the real math produced, at a fraction of the wait. The override is never quiet about itself: the demo prints <code>*** LOCAL-DEMO-SCALE RUN ***</code> the moment it&#8217;s active, specifically so a reader watching the output never mistakes today&#8217;s fast local numbers for the real ones.</p><h2>A verification note, honestly</h2><p>This lesson&#8217;s own generation happened in an environment with no Docker installed at all &#8212; not a Testcontainers-can&#8217;t-reach-Docker situation, no Docker binary present, period. <code>TopicBootstrapperIT</code> is written and included as the real, portable, correct verification &#8212; it&#8217;s exactly what would run on any standard machine with Docker &#8212; but it was not actually executed to produce this lesson. That gap is stated here rather than papered over; run it yourself, and <code>mvn -pl streamsocial-common -am verify</code> is where to start.</p><h1>Implementation Guide:</h1><h2>GitHub Link :</h2><p><a href="https://github.com/sysdr/streamsocial-java/tree/main/day03-v2-streamsocial-source/streamsocial">https://github.com/sysdr/streamsocial-java/tree/main/day03-v2-streamsocial-source/streamsocial</a></p><h2>Build, Test, and Demo Topic Provisioning</h2><h2>The idea, in pseudo-code</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;97d2f5da-2e0a-4e27-9c3e-faf98411d2a3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">TopicSpec(name, partitions, replicationFactor, configs)

catalog = [
    TopicSpec("user-actions",         partitions=1000, rf=3),  # 12 at local-demo-scale
    TopicSpec("content-interactions", partitions=500,  rf=3),  #  6 at local-demo-scale
]

bootstrap(desired):                       # Appendix C's proven shape
    existing = adminClient.listTopics()
    missing  = desired where name not in existing
    if missing: adminClient.createTopics(missing)

    for spec in present (not missing):    # this course's extension
        actual = adminClient.describeTopic(spec.name).partitionCount
        report verified or mismatched</code></pre></div><p>Two calls to the cluster &#8212; list, then either create or describe &#8212; and everything else is comparing what came back against the catalog.</p><h2>Step 1 &#8212; Make sure the Day 2 cluster is running</h2><pre><code><code>cd streamsocial-infra
./scripts/start.sh</code></code></pre><p>If it&#8217;s already up from Day 2, this is a no-op.</p><h2>Step 2 &#8212; Run the bootstrap at local-demo-scale</h2><pre><code><code>cd ..
mvn org.codehaus.mojo:exec-maven-plugin:3.3.0:java -pl streamsocial-common \
  -Dexec.mainClass=com.streamsocial.common.demo.TopicBootstrapDemo \
  -Dstreamsocial.topics.user-actions-partitions=12 \
  -Dstreamsocial.topics.content-interactions-partitions=6</code></code></pre><p><strong>Expected output (first run):</strong></p><pre><code><code>*** LOCAL-DEMO-SCALE RUN ***
Partition counts below are overridden for a fast local check.
The real production defaults are 1000 (user-actions) / 500 (content-interactions).
...
Created:            [user-actions, content-interactions]
Already present ok:  []
Mismatched:          []</code></code></pre><p>Run the exact same command again:</p><p><strong>Expected output (second run):</strong></p><pre><code><code>Created:            []
Already present ok:  [user-actions, content-interactions]
Mismatched:          []</code></code></pre><p>That difference between the two runs is the whole point &#8212; same command, different (correct) behavior depending on cluster state.</p><h2>Step 3 &#8212; Run the real production numbers, once, to see the difference</h2><pre><code><code>mvn org.codehaus.mojo:exec-maven-plugin:3.3.0:java -pl streamsocial-common \
  -Dexec.mainClass=com.streamsocial.common.demo.TopicBootstrapDemo</code></code></pre><p>This attempts to create <code>user-actions</code> at the real 1000 partitions &#8212; expect it to take noticeably longer than Step 2, and note that it will report a <strong>mismatch</strong> against the 12/6-partition topics Step 2 already created (Kafka can&#8217;t shrink or silently grow partitions). Use <code>./scripts/stop.sh --wipe</code> and start fresh if you want to try this cleanly.</p><h2>Step 4 &#8212; Verify independently from the CLI</h2><pre><code><code>cd streamsocial-infra
./scripts/list-topics.sh</code></code></pre><p><strong>Expected output:</strong> both topic names listed, with <code>PartitionCount</code> matching whichever run (local-demo-scale or production) actually created them. This uses <code>kafka-topics</code> directly against the broker, not the Java <code>AdminClient</code> code path &#8212; two independent tools agreeing is a real check.</p><h2>Step 5 &#8212; Run the tests</h2><pre><code><code>mvn -pl streamsocial-common -am verify</code></code></pre><p><code>StreamSocialTopicsTest</code> runs under Surefire, no Docker needed. <code>TopicBootstrapperIT</code> &#8212; note the <code>IT</code> suffix, not <code>Test</code> &#8212; runs under Failsafe during the <code>verify</code> phase, spins up a real single-node broker via Testcontainers, and needs Docker running. If you only run <code>mvn test</code>, this class is silently skipped; that&#8217;s Failsafe&#8217;s convention, not a bug.</p><p><strong>Expected:</strong> all green.</p><h2>Success criteria </h2><p>Run <code>TopicBootstrapDemo</code> against the Day 2 cluster and confirm both topics get created. Run it again immediately and confirm the second run creates nothing &#8212; both topics report as already present and verified. Then explain, in one sentence each, why <code>user-actions</code> got 1000 partitions and why <code>content-interactions</code> didn&#8217;t get the full 667 the raw math suggested.</p><h2>Working Demo Link :</h2><div id="youtube2-NGOohQW2tFM" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;NGOohQW2tFM&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/NGOohQW2tFM?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><h2>Homework</h2><p>Add a third topic to the catalog: <code>moderation-events</code>, sized for an assumed peak of 2M events/sec using the same 30,000 events/sec-per-partition budget this lesson used for the other two &#8212; but don&#8217;t cap it below the raw math this time. Justify in a one-line comment why <code>moderation-events</code> doesn&#8217;t need the same treatment <code>content-interactions</code> got, and add a local-demo-scale override for it too, following the existing pattern.</p><p>Implementation checklist:</p><ol><li><p>Add a <code>MODERATION_EVENTS</code> constant to <code>StreamSocialTopics</code>, computing the partition count from the stated peak and budget, with its own <code>resolvePartitions(...)</code> call and system property key.</p></li><li><p>Add it to <code>StreamSocialTopics.ALL</code>.</p></li><li><p>Add a test asserting the catalog grew to 3 entries and the new topic&#8217;s default partition count matches your math.</p></li><li><p>Re-run the bootstrap demo and <code>list-topics.sh</code> and confirm the new topic appears.</p></li></ol><h2>Solution hints</h2><ul><li><p><code>2,000,000 / 30,000 &#8776; 67</code> &#8212; round to a clean number like 64 or 70, showing the arithmetic in a comment rather than picking a bare number.</p></li><li><p><code>moderation-events</code> doesn&#8217;t need capping because it&#8217;s a <code>SystemEvent</code> type (Day 1) keyed by <code>subjectId</code> &#8212; moderation flags aren&#8217;t subject to the same viral power-law skew as user-generated content interactions.</p></li><li><p>Keep the local-demo-scale ratio consistent &#8212; if the real default is ~70, a local override around 2-3 keeps the same rough proportion to the other two topics&#8217; 12/6.</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://handsonkafka.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hands On Kafka  is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Day 2: Kafka Cluster Setup]]></title><description><![CDATA[What we&#8217;ll build today]]></description><link>https://handsonkafka.substack.com/p/day-2-kafka-cluster-setup</link><guid isPermaLink="false">https://handsonkafka.substack.com/p/day-2-kafka-cluster-setup</guid><dc:creator><![CDATA[Kafka]]></dc:creator><pubDate>Wed, 05 Aug 2026 08:31:33 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!DVT9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45bdee1-48b4-4974-a9c6-d42af91a8f49_1125x775.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>What we&#8217;ll build today</h2><blockquote><p>A real 3-broker Kafka cluster running in KRaft mode &#8212; no Zookeeper anywhere in the stack</p><p><code>streamsocial-infra</code>: Docker Compose, health checks, and scripts, sitting alongside <code>streamsocial-common</code> from Day 1</p><p>A controller quorum you can actually kill a node in and watch re-elect itself</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DVT9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45bdee1-48b4-4974-a9c6-d42af91a8f49_1125x775.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DVT9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45bdee1-48b4-4974-a9c6-d42af91a8f49_1125x775.png 424w, https://substackcdn.com/image/fetch/$s_!DVT9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45bdee1-48b4-4974-a9c6-d42af91a8f49_1125x775.png 848w, https://substackcdn.com/image/fetch/$s_!DVT9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45bdee1-48b4-4974-a9c6-d42af91a8f49_1125x775.png 1272w, https://substackcdn.com/image/fetch/$s_!DVT9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45bdee1-48b4-4974-a9c6-d42af91a8f49_1125x775.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DVT9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45bdee1-48b4-4974-a9c6-d42af91a8f49_1125x775.png" width="506" height="348.5777777777778" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f45bdee1-48b4-4974-a9c6-d42af91a8f49_1125x775.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:775,&quot;width&quot;:1125,&quot;resizeWidth&quot;:506,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!DVT9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45bdee1-48b4-4974-a9c6-d42af91a8f49_1125x775.png 424w, https://substackcdn.com/image/fetch/$s_!DVT9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45bdee1-48b4-4974-a9c6-d42af91a8f49_1125x775.png 848w, https://substackcdn.com/image/fetch/$s_!DVT9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45bdee1-48b4-4974-a9c6-d42af91a8f49_1125x775.png 1272w, https://substackcdn.com/image/fetch/$s_!DVT9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff45bdee1-48b4-4974-a9c6-d42af91a8f49_1125x775.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Why KRaft, not Zookeeper</h2><p>For most of Kafka&#8217;s life, the cluster&#8217;s metadata &#8212; which broker leads which partition, which topics exist, which consumer groups own what &#8212; lived in a separate Zookeeper ensemble. That meant running and operating two distributed systems to get one working Kafka cluster: Kafka itself, and Zookeeper underneath it holding the metadata Kafka depended on to function at all.</p><p>KRaft (Kafka Raft) removes that second system. Kafka&#8217;s own brokers &#8212; or a designated subset of them &#8212; now run a Raft consensus protocol directly to agree on cluster metadata. One system, one thing to operate, one thing to reason about when something goes wrong. This isn&#8217;t a minor internal refactor; it&#8217;s the reason a 3-node cluster today is three Kafka containers instead of three Kafka containers plus three (or five) Zookeeper containers watching over them.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://handsonkafka.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hands On Kafka  is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Broker roles and the controller quorum</h2><p>In KRaft mode, every node runs with a <code>process.roles</code> setting of <code>broker</code>, <code>controller</code>, or both. A <strong>broker</strong> serves client reads and writes &#8212; the thing producers and consumers actually talk to. A <strong>controller</strong> participates in the metadata Raft quorum &#8212; deciding partition leadership, tracking which brokers are alive, and persisting the cluster&#8217;s source of truth.</p><p>For a cluster this size, every node runs both roles at once &#8212; <code>broker,controller</code> &#8212; which is the standard shape for development and teaching clusters. Larger production deployments typically split dedicated controller-only nodes out from the brokers serving traffic.</p><p>What matters today is the <strong>controller quorum</strong>: the set of nodes voting on metadata changes via <code>controller.quorum.voters</code>. With three voters, any two form a majority &#8212; which is exactly what makes today&#8217;s challenge possible. Kill one voter, including the current leader, and the remaining two keep the cluster&#8217;s metadata consistent and elect a new active controller among themselves.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!oLXb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1f84d6f-7d3b-4344-b292-389cc8c5efb1_850x850.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!oLXb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1f84d6f-7d3b-4344-b292-389cc8c5efb1_850x850.png 424w, https://substackcdn.com/image/fetch/$s_!oLXb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1f84d6f-7d3b-4344-b292-389cc8c5efb1_850x850.png 848w, https://substackcdn.com/image/fetch/$s_!oLXb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1f84d6f-7d3b-4344-b292-389cc8c5efb1_850x850.png 1272w, https://substackcdn.com/image/fetch/$s_!oLXb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1f84d6f-7d3b-4344-b292-389cc8c5efb1_850x850.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!oLXb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1f84d6f-7d3b-4344-b292-389cc8c5efb1_850x850.png" width="410" height="410" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e1f84d6f-7d3b-4344-b292-389cc8c5efb1_850x850.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:850,&quot;width&quot;:850,&quot;resizeWidth&quot;:410,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!oLXb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1f84d6f-7d3b-4344-b292-389cc8c5efb1_850x850.png 424w, https://substackcdn.com/image/fetch/$s_!oLXb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1f84d6f-7d3b-4344-b292-389cc8c5efb1_850x850.png 848w, https://substackcdn.com/image/fetch/$s_!oLXb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1f84d6f-7d3b-4344-b292-389cc8c5efb1_850x850.png 1272w, https://substackcdn.com/image/fetch/$s_!oLXb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe1f84d6f-7d3b-4344-b292-389cc8c5efb1_850x850.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Two listener ports per broker, and why that split exists</h2><p>Each broker in <code>docker-compose.yml</code> declares two listeners: an internal one on <code>:19092</code> for inter-broker traffic, and an external <code>PLAINTEXT_HOST</code> listener on its own host-mapped port (<code>9092</code>, <code>9093</code>, <code>9094</code>). The internal listener is never exposed to the host at all &#8212; it exists purely so brokers can replicate data and coordinate with each other over the Docker network. Every host-side client this course builds &#8212; the AdminClient in Day 3, every producer and consumer after that, this lesson&#8217;s own verification scripts &#8212; talks to a broker&#8217;s external listener instead. Collapsing these into one listener is a common shortcut that works fine for a single-broker toy setup and breaks in subtle ways the moment replication and inter-broker traffic are real, which they are here from day one.</p><p><code>KAFKA_AUTO_CREATE_TOPICS_ENABLE</code> is set to <code>false</code> on every broker, deliberately. Without it, the very first producer or consumer to reference a topic name that doesn&#8217;t exist yet would silently create one &#8212; with whatever default partition count the broker happens to be configured with, not the number Day 3&#8217;s math actually calls for. Turning this off forces every topic through the idempotent <code>AdminClient</code> bootstrap Day 3 builds, which is exactly where that decision belongs.</p><h2>What actually happens when you kill the leader</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yCyV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04db6d05-0127-4dd8-851f-4850fe4c672c_1025x525.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yCyV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04db6d05-0127-4dd8-851f-4850fe4c672c_1025x525.png 424w, https://substackcdn.com/image/fetch/$s_!yCyV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04db6d05-0127-4dd8-851f-4850fe4c672c_1025x525.png 848w, https://substackcdn.com/image/fetch/$s_!yCyV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04db6d05-0127-4dd8-851f-4850fe4c672c_1025x525.png 1272w, https://substackcdn.com/image/fetch/$s_!yCyV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04db6d05-0127-4dd8-851f-4850fe4c672c_1025x525.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yCyV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04db6d05-0127-4dd8-851f-4850fe4c672c_1025x525.png" width="510" height="261.219512195122" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/04db6d05-0127-4dd8-851f-4850fe4c672c_1025x525.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:525,&quot;width&quot;:1025,&quot;resizeWidth&quot;:510,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yCyV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04db6d05-0127-4dd8-851f-4850fe4c672c_1025x525.png 424w, https://substackcdn.com/image/fetch/$s_!yCyV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04db6d05-0127-4dd8-851f-4850fe4c672c_1025x525.png 848w, https://substackcdn.com/image/fetch/$s_!yCyV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04db6d05-0127-4dd8-851f-4850fe4c672c_1025x525.png 1272w, https://substackcdn.com/image/fetch/$s_!yCyV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04db6d05-0127-4dd8-851f-4850fe4c672c_1025x525.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Today&#8217;s challenge &#8212; verify controller failover &#8212; has three parts:</p><ol><li><p><strong>Find the active controller.</strong> <code>kafka-metadata-quorum describe --status</code> reports a <code>LeaderId</code> among the three voters. That&#8217;s the node currently deciding metadata changes.</p></li><li><p><strong>Kill it.</strong> Not a graceful shutdown &#8212; <code>docker kill</code>, the same as pulling power on a physical machine. This is the failure mode worth testing, because graceful shutdowns are the easy case.</p></li><li><p><strong>Watch the survivors.</strong> With node availability down to two out of three, the surviving voters still hold a majority. They detect the missing leader, run a new election, and one of them becomes the new <code>LeaderId</code> &#8212; typically within a few seconds.</p></li></ol><p>The cluster&#8217;s client-facing brokers keep answering requests through all of this, because broker availability and controller leadership are separate concerns &#8212; losing the active controller doesn&#8217;t take the cluster offline, it just changes who&#8217;s writing the metadata log.</p><h1>Implementation Guide: </h1><h3>GitHub Link :</h3><p><a href="https://github.com/sysdr/streamsocial-java/tree/main/day02-v2-streamsocial-source/streamsocial">https://github.com/sysdr/streamsocial-java/tree/main/day02-v2-streamsocial-source/streamsocial</a></p><h2>Build, Verify, and Demo the Kafka Cluster</h2><h2>The idea, in pseudo-code</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;bb7223d9-0e79-4c8d-8bb4-6ef8b21b2a4b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">cluster = 3 nodes, each: process.roles = broker + controller
quorum_voters = {1@kafka-1:19093, 2@kafka-2:19093, 3@kafka-3:19093}

start():
    for each node: format storage with shared CLUSTER_ID, join quorum
    wait until all nodes report healthy

describe_status():
    ask any broker: "who is the current controller leader?"
    -&gt; LeaderId, CurrentVoters, LeaderEpoch

failover_demo():
    leader = describe_status().LeaderId
    kill(container_for(leader))
    poll a surviving node's describe_status() until LeaderId changes
    -&gt; failover confirmed
    restart(container_for(leader))  # rejoins as follower</code></pre></div><p>Three nodes, one shared quorum, one script that proves the quorum survives losing a member.</p><h2>Step 1 &#8212; Unpack and start everything</h2><pre><code><code>cd streamsocial
./start.sh</code></code></pre><p><strong>Expected output:</strong> Maven dependency resolution, <code>BUILD SUCCESS</code> on <code>streamsocial-common</code>&#8216;s tests, then all three <code>streamsocial-kafka-N</code> containers reporting <code>healthy</code>, ending with a cluster verification block. First run pulls the <code>confluentinc/cp-kafka:7.7.1</code> image, so expect a longer wait the very first time.</p><h2>Step 2 &#8212; Verify brokers and quorum directly</h2><pre><code><code>cd streamsocial-infra
./scripts/verify-cluster.sh</code></code></pre><p><strong>Expected output:</strong> an API version response from each broker, then a <code>describe --status</code> block. Confirm:</p><ul><li><p><code>CurrentVoters</code> lists node IDs <code>1</code>, <code>2</code>, <code>3</code></p></li><li><p>Exactly one <code>LeaderId</code> is reported</p></li></ul><p>If any broker fails its health check, check its logs:</p><pre><code><code>docker compose -f "$(pwd)/docker-compose.yml" logs kafka-2</code></code></pre><p>A common first-run cause is insufficient Docker memory &#8212; bump Docker Desktop&#8217;s allocation to at least 4GB and retry.</p><h2>Step 3 &#8212; Demo controller failover (today&#8217;s challenge)</h2><pre><code><code>./scripts/demo-failover.sh</code></code></pre><p><strong>Expected output:</strong> the current quorum status, a line identifying and killing the active controller&#8217;s container, then a second <code>describe --status</code> block with a <strong>different</strong> <code>LeaderId</code> than the first. The script restarts the killed container automatically at the end.</p><p>Run <code>verify-cluster.sh</code> again afterward &#8212; all three nodes should show healthy and back in the voter set.</p><h2>Step 4 &#8212; Stop everything</h2><pre><code><code>cd ..
./stop.sh</code></code></pre><p>Data volumes persist by default, so a later <code>./start.sh</code> picks the cluster back up with the same metadata. Use <code>./stop.sh --wipe</code> to delete the volumes and force a clean re-format on next start.</p><h2>Success criteria </h2><p><code>./scripts/verify-cluster.sh</code> shows all three brokers healthy and all three node IDs as controller quorum voters. <code>./scripts/demo-failover.sh</code> shows a <code>LeaderId</code> change after the active controller&#8217;s container is killed, and the killed node rejoining as a follower once restarted. This is a pure infrastructure lesson with no application-level behavior yet, so today&#8217;s proof lives entirely in these two scripts&#8217; terminal output &#8212; the live dashboard doesn&#8217;t arrive until Day 4, once there&#8217;s a running service worth watching.</p><h2>Working Demo Link :</h2><div id="youtube2-3t6Ha83PMlM" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;3t6Ha83PMlM&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/3t6Ha83PMlM?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><h2>Homework</h2><p>Modify <code>docker-compose.yml</code> to run a <strong>5-broker</strong> cluster instead of 3, updating <code>KAFKA_CONTROLLER_QUORUM_VOTERS</code> and adding <code>kafka-4</code> and <code>kafka-5</code> service definitions with their own reserved external ports.</p><p>Implementation checklist:</p><ol><li><p>Extend the <code>x-kafka-common</code> anchor&#8217;s <code>KAFKA_CONTROLLER_QUORUM_VOTERS</code> to list all 5 node IDs on their internal <code>:19093</code> controller listener.</p></li><li><p>Add <code>kafka-4</code> and <code>kafka-5</code> service blocks following the existing three as a template &#8212; new <code>KAFKA_NODE_ID</code>, new external host port (<code>9095</code>, <code>9096</code> &#8212; check these aren&#8217;t already claimed before using them elsewhere), new named volume.</p></li><li><p>Update <code>scripts/start.sh</code>&#8216;s health-check loop to include the two new services.</p></li><li><p>Run the failover demo against the 5-node cluster and kill <strong>two</strong> nodes in a row &#8212; with 5 voters, a majority is 3, so the cluster should survive losing 2.</p></li></ol><h2>Solution hints</h2><ul><li><p>The <code>CLUSTER_ID</code> stays the same across all 5 nodes &#8212; it identifies the cluster, not a specific node.</p></li><li><p>Each new broker needs a unique <code>KAFKA_NODE_ID</code>, a unique external port, and its own volume &#8212; reusing a port or volume name collides with an existing container.</p></li><li><p>Killing 2 out of 5 should still leave a working quorum (3 remaining voters, still a majority); killing 3 out of 5 should not &#8212; try that too and observe <code>describe --status</code> stall until a voter comes back, which is the concrete meaning of &#8220;quorum lost.&#8221;</p></li><li><p>If a new port you pick for <code>kafka-4</code>/<code>kafka-5</code> turns out to already be reserved for something later in the course, pick a different one and note the change &#8212; the reserved-ports list only helps if it&#8217;s kept current.</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://handsonkafka.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hands On Kafka  is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Day 1: Event-Driven Architecture Fundamentals]]></title><description><![CDATA[What we&#8217;re building today]]></description><link>https://handsonkafka.substack.com/p/day-1-event-driven-architecture-fundamentals-68d</link><guid isPermaLink="false">https://handsonkafka.substack.com/p/day-1-event-driven-architecture-fundamentals-68d</guid><dc:creator><![CDATA[Kafka]]></dc:creator><pubDate>Sat, 01 Aug 2026 08:30:34 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!62Lh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20492cec-3c2c-4c44-855f-17fa205f4d7a_1025x575.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>What we&#8217;re building today</h2><blockquote><p>A shared <code>DomainEvent</code> sealed interface &#8212; the single root every fact about StreamSocial will ever hang off of</p><p>Three event categories under it: <code>UserActionEvent</code>, <code>ContentInteractionEvent</code>, <code>SystemEvent</code></p><p>Eight concrete, immutable Java records (three, three, and two respectively) with Jakarta Bean Validation constraints baked in</p><p>The <code>streamsocial-common</code> Maven module &#8212; the one dependency every other module in this 60-day build will import from</p><p>No Kafka broker today. No Spring Boot app today. Today is entirely about the shape of the data before a single byte of it touches a topic &#8212; because the taxonomy you commit to on Day 1 is the one 53 more lessons of producers, consumers, and Streams topologies will all key off of.</p></blockquote><h2>Where this fits in StreamSocial</h2><blockquote><p><code>streamsocial-common</code> is the only module every other module in the system depends on &#8212; the producer service (Day 4) serializes these records onto topics, the engagement consumer (Day 5) deserializes them back, and the Kafka Streams topology (Day 41) pattern-matches over them to compute trending scores. Get the taxonomy wrong here and you&#8217;re fighting schema evolution on Day 29; get it right, and Days 2 through 60 just import it.</p><p>That&#8217;s also why the Bean Validation annotations live directly on the records instead of in some separate &#8220;validation service&#8221; &#8212; the constraint that a post&#8217;s content can&#8217;t be blank is a property of what a <code>PostCreated</code> event <em>is</em>, not a rule some other layer enforces on it later.</p></blockquote><h2>Core concept: an event taxonomy is a contract, not a formality</h2><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AJa8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb78659-ec77-4ceb-9997-cb03da37574a_1025x325.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AJa8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb78659-ec77-4ceb-9997-cb03da37574a_1025x325.png 424w, https://substackcdn.com/image/fetch/$s_!AJa8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb78659-ec77-4ceb-9997-cb03da37574a_1025x325.png 848w, https://substackcdn.com/image/fetch/$s_!AJa8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb78659-ec77-4ceb-9997-cb03da37574a_1025x325.png 1272w, https://substackcdn.com/image/fetch/$s_!AJa8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb78659-ec77-4ceb-9997-cb03da37574a_1025x325.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AJa8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb78659-ec77-4ceb-9997-cb03da37574a_1025x325.png" width="548" height="173.7560975609756" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6eb78659-ec77-4ceb-9997-cb03da37574a_1025x325.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:325,&quot;width&quot;:1025,&quot;resizeWidth&quot;:548,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!AJa8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb78659-ec77-4ceb-9997-cb03da37574a_1025x325.png 424w, https://substackcdn.com/image/fetch/$s_!AJa8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb78659-ec77-4ceb-9997-cb03da37574a_1025x325.png 848w, https://substackcdn.com/image/fetch/$s_!AJa8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb78659-ec77-4ceb-9997-cb03da37574a_1025x325.png 1272w, https://substackcdn.com/image/fetch/$s_!AJa8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eb78659-ec77-4ceb-9997-cb03da37574a_1025x325.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><blockquote><p>Request-response systems ask &#8220;what do you want right now?&#8221; Event-driven systems ask &#8220;what just happened?&#8221; &#8212; and every downstream consumer, from Day 5&#8217;s engagement consumer to Day 44&#8217;s trending-hashtag topology, only knows what happened because the <em>shape</em> of that event told it precisely, with no ambiguity.</p><p>Java&#8217;s <code>sealed</code> keyword turns that shape into something the compiler enforces, not just something a wiki page describes. <code>DomainEvent permits UserActionEvent, ContentInteractionEvent, SystemEvent</code> means there is no fourth category anyone can quietly add without touching this file &#8212; and every record component (<code>eventId</code>, <code>occurredAt</code>, <code>postId</code>, <code>content</code>) is <code>final</code> by construction, because Java records give you immutability for free.</p></blockquote><h2>Architecture: from action to accepted (or rejected) fact</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!62Lh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20492cec-3c2c-4c44-855f-17fa205f4d7a_1025x575.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!62Lh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20492cec-3c2c-4c44-855f-17fa205f4d7a_1025x575.png 424w, https://substackcdn.com/image/fetch/$s_!62Lh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20492cec-3c2c-4c44-855f-17fa205f4d7a_1025x575.png 848w, https://substackcdn.com/image/fetch/$s_!62Lh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20492cec-3c2c-4c44-855f-17fa205f4d7a_1025x575.png 1272w, https://substackcdn.com/image/fetch/$s_!62Lh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20492cec-3c2c-4c44-855f-17fa205f4d7a_1025x575.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!62Lh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20492cec-3c2c-4c44-855f-17fa205f4d7a_1025x575.png" width="516" height="289.4634146341463" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/20492cec-3c2c-4c44-855f-17fa205f4d7a_1025x575.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:575,&quot;width&quot;:1025,&quot;resizeWidth&quot;:516,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!62Lh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20492cec-3c2c-4c44-855f-17fa205f4d7a_1025x575.png 424w, https://substackcdn.com/image/fetch/$s_!62Lh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20492cec-3c2c-4c44-855f-17fa205f4d7a_1025x575.png 848w, https://substackcdn.com/image/fetch/$s_!62Lh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20492cec-3c2c-4c44-855f-17fa205f4d7a_1025x575.png 1272w, https://substackcdn.com/image/fetch/$s_!62Lh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20492cec-3c2c-4c44-855f-17fa205f4d7a_1025x575.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A user action becomes a <code>DomainEvent</code> in three steps: something happens, a record gets constructed with that action&#8217;s specific fields, and Jakarta Bean Validation checks every <code>@NotNull</code>/<code>@NotBlank</code>/<code>@Size</code> constraint before the event is allowed to be considered &#8220;real.&#8221; Nothing here talks to Kafka yet &#8212; that constructed, validated record is exactly what Day 4&#8217;s producer will hand to <code>KafkaTemplate.send(...)</code>.</p><p>The validation step isn&#8217;t a one-shot check &#8212; it&#8217;s a small state machine every event instance passes through: <code>Constructed &#8594; Validating &#8594; Valid</code> or <code>Constructed &#8594; Validating &#8594; Invalid</code>. Both are terminal today; there&#8217;s no retry loop, because a malformed event isn&#8217;t a transient failure, it&#8217;s a bug in the caller.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://handsonkafka.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hands On Kafka  is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!uMHQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c48b3f6-1ed8-4724-b84b-8e3bd44997c4_1025x475.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!uMHQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c48b3f6-1ed8-4724-b84b-8e3bd44997c4_1025x475.png 424w, https://substackcdn.com/image/fetch/$s_!uMHQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c48b3f6-1ed8-4724-b84b-8e3bd44997c4_1025x475.png 848w, https://substackcdn.com/image/fetch/$s_!uMHQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c48b3f6-1ed8-4724-b84b-8e3bd44997c4_1025x475.png 1272w, https://substackcdn.com/image/fetch/$s_!uMHQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c48b3f6-1ed8-4724-b84b-8e3bd44997c4_1025x475.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!uMHQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c48b3f6-1ed8-4724-b84b-8e3bd44997c4_1025x475.png" width="514" height="238.1951219512195" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8c48b3f6-1ed8-4724-b84b-8e3bd44997c4_1025x475.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:475,&quot;width&quot;:1025,&quot;resizeWidth&quot;:514,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!uMHQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c48b3f6-1ed8-4724-b84b-8e3bd44997c4_1025x475.png 424w, https://substackcdn.com/image/fetch/$s_!uMHQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c48b3f6-1ed8-4724-b84b-8e3bd44997c4_1025x475.png 848w, https://substackcdn.com/image/fetch/$s_!uMHQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c48b3f6-1ed8-4724-b84b-8e3bd44997c4_1025x475.png 1272w, https://substackcdn.com/image/fetch/$s_!uMHQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c48b3f6-1ed8-4724-b84b-8e3bd44997c4_1025x475.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h2>Implementation Guide :</h2><h3>GitHub Link :</h3><p><a href="https://github.com/sysdr/streamsocial-java/tree/main/streamsocial-day01/streamsocial">https://github.com/sysdr/streamsocial-java/tree/main/streamsocial-day01/streamsocial</a></p><h2>Fundamentals first</h2><p>An event is a fact, not a command &#8212; <code>PostCreated</code> says a post <em>was</em> created, it doesn&#8217;t ask anyone to create one. That distinction is why event-driven systems scale differently than request-response ones: nobody has to be listening at the moment the fact happens for the fact to be true.</p><p>Java gives you two tools to make that fact durable and unambiguous: <strong>records</strong> (all fields final, <code>equals</code>/<code>hashCode</code>/<code>toString</code> generated, no setters &#8212; a fact can&#8217;t un-happen) and <strong>sealed interfaces</strong> (a fixed, compiler-known list of what a &#8220;fact&#8221; can be).</p><h2>Today&#8217;s specific pattern: sealed hierarchy + Bean Validation</h2><p>Pseudo-code shape of what you&#8217;re building &#8212; not the real file, just the skeleton:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;acb47e35-f2b2-4e93-b237-79b370d8592a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">sealed interface DomainEvent permits UserActionEvent, ContentInteractionEvent, SystemEvent
    eventId(): UUID
    occurredAt(): Instant

sealed interface UserActionEvent extends DomainEvent permits PostCreated, PostDeleted, ProfileUpdated

record PostCreated(eventId, occurredAt, userId, postId, @NotBlank content) implements UserActionEvent</code></pre></div><p>The validation constraints (<code>@NotNull</code>, <code>@NotBlank</code>, <code>@Size</code>) sit directly on the record&#8217;s constructor parameters. Jakarta&#8217;s <code>Validator.validate(event)</code> walks those annotations at runtime and hands back a <code>Set&lt;ConstraintViolation&lt;T&gt;&gt;</code> &#8212; empty means valid.</p><h2>How it snaps into the system</h2><p>Nothing yet &#8212; that&#8217;s the point of Day 1. <code>streamsocial-common</code> has zero dependencies on any other StreamSocial module; every module from Day 4 onward depends on <em>it</em>.</p><h2>Build, test, and demo &#8212; step by step</h2><p><strong>1. Build the reactor and run the tests</strong></p><pre><code><code>cd streamsocial
mvn -pl streamsocial-common -am verify</code></code></pre><p>Expected output (tail of it):</p><pre><code><code>Tests run: 5, Failures: 0, Errors: 0 -- in ...DomainEventValidationTest
Tests run: 3, Failures: 0, Errors: 0 -- in ...DomainEventDescriberTest
Tests run: 8, Failures: 0, Errors: 0
BUILD SUCCESS</code></code></pre><p><strong>2. Run the one-command lifecycle script</strong></p><pre><code><code>./start.sh</code></code></pre><p>Expected: prerequisite checks pass, the reactor builds, and since neither Docker infra (Day 2) nor the dashboard (Day 4) exist yet in this snapshot, both are skipped with a message rather than an error &#8212; then the terminal demo runs and prints all 8 event types plus one rejected validation.</p><p><strong>3. Run just the demo, without rebuilding</strong></p><pre><code><code>mvn -q org.codehaus.mojo:exec-maven-plugin:3.3.0:java \
    -pl streamsocial-common \
    -Dexec.mainClass=com.streamsocial.common.event.EventTaxonomyDemo \
    -Dexec.classpathScope=test</code></code></pre><p>Expected: the same 8-line taxonomy printout plus <code>REJECTED: content must not be blank</code>.</p><p><strong>4. Docker note</strong></p><p>There is no Docker step today &#8212; <code>streamsocial-infra</code> doesn&#8217;t exist until Day 2. <code>start.sh</code> already knows to skip it; you don&#8217;t need to do anything differently with or without Docker installed for this lesson.</p><p><strong>5. Shut down cleanly</strong></p><pre><code><code>./stop.sh</code></code></pre><p>Expected: Maven build output cleaned, no errors, safe to run again even if nothing was running.</p><h2>Success criterion</h2><p>Run <code>./start.sh</code> from the repository root. You should see a build that compiles cleanly, 8 passing JUnit 5 tests, and a terminal demo that prints all eight event types by name plus one deliberately-blank <code>PostCreated</code> getting rejected with <code>content must not be blank</code>. If you can explain out loud why <code>DomainEvent</code> is <code>sealed</code> instead of just a plain <code>interface</code> &#8212; and what breaks on Day 44 if it weren&#8217;t &#8212; you&#8217;ve got today&#8217;s concept.</p><h2>Working Demo Link :</h2><div id="youtube2-lYaYbo8VtTc" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;lYaYbo8VtTc&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/lYaYbo8VtTc?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><h2>Homework assignment</h2><p>Extend the taxonomy to the full 10 event types the Day 1 challenge calls for, without breaking anything that exists:</p><ol><li><p>Add two new leaf records: <code>PostEdited</code> (under <code>UserActionEvent</code> &#8212; a user changed a post&#8217;s content after publishing) and <code>PostReported</code> (under <code>ContentInteractionEvent</code> &#8212; a user flagged someone else&#8217;s post).</p></li><li><p>Give each at least one meaningful <code>@NotNull</code>/<code>@NotBlank</code>/<code>@Size</code> constraint beyond the shared <code>eventId</code>/<code>occurredAt</code>.</p></li><li><p>Update the <code>permits</code> clause on the relevant category interface &#8212; the compiler will now force you to handle the new type anywhere a switch or instanceof chain claims to be exhaustive.</p></li><li><p>Add a validation test and a <code>DomainEventDescriber</code> branch for each new type; run <code>mvn -pl streamsocial-common -am verify</code> and confirm the test count goes from 8 to 10+.</p></li></ol><h2>Solution hints</h2><ul><li><p><code>PostEdited</code> needs its own <code>postId</code> and the <em>new</em> content, following the exact same field pattern as <code>PostCreated</code> &#8212; copy its constraint style, don&#8217;t invent a new one.</p></li><li><p><code>PostReported</code> needs <code>reporterId</code>, <code>postId</code>, and a <code>@NotBlank reason</code> &#8212; resist adding an enum for report categories today, that&#8217;s more taxonomy than Day 1 needs.</p></li><li><p>If <code>DomainEventDescriber.describe(...)</code> doesn&#8217;t get a new <code>else if</code> branch for each new type, its final <code>throw new IllegalStateException(...)</code> is your safety net &#8212; it should fire in a test if you forget one, not silently return a wrong description.</p></li><li><p>Don&#8217;t touch <code>DomainEvent</code>, <code>UserActionEvent</code>&#8216;s or <code>ContentInteractionEvent</code>&#8216;s existing <code>permits</code> entries &#8212; only <em>add</em> to them.</p></li></ul><h2></h2><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://handsonkafka.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hands On Kafka  is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Kafka Mastery: Building StreamSocial — Java & Spring Boot Edition]]></title><description><![CDATA[Introduction]]></description><link>https://handsonkafka.substack.com/p/kafka-mastery-building-streamsocial-4e3</link><guid isPermaLink="false">https://handsonkafka.substack.com/p/kafka-mastery-building-streamsocial-4e3</guid><dc:creator><![CDATA[Kafka]]></dc:creator><pubDate>Fri, 24 Jul 2026 08:30:58 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!EaU2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02526676-5f58-433a-9c9d-1a6b6ac572c5_1270x832.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Introduction </h2><p>Event-driven systems are one of the most deceptively difficult problems in backend engineering. Every non-trivial platform eventually needs to move data between services in real time, fan events out to dozens of downstream consumers, or guarantee that a payment event is processed exactly once &#8212; and the moment a system moves past a single-broker tutorial setup, the simple producer-and-consumer pattern that worked in a five-minute quickstart starts causing duplicate processing, out-of-order writes, and silent data loss in production.</p><p>This course was built to address that exact gap. It takes learners from a three-broker Kafka cluster and a first producer all the way to a secured, observable, production-hardened event-driven platform &#8212; the kind of system that underlies real-time feeds, activity streams, and event backbones at companies operating at genuine scale. Rather than treating Kafka as a message queue with extra configuration, this program treats it as a complete systems-design subject, covering producer and consumer internals, schema management, Kafka Connect, Kafka Streams, observability, and security along the way.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://handsonkafka.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hands On Kafka  is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>The material is organized as a 60-day publishing plan, with each day representing a focused lesson that builds directly on the one before it, inside a single continuously growing codebase. Java 17 and Spring Boot are the implementation language throughout &#8212; the JVM-native, production-standard way real teams actually run Kafka clients &#8212; but Kafka itself is the subject of every lesson. By the end, learners will have designed, coded, and operated a distributed, event-driven social platform called StreamSocial, comparable in shape to the real-time backbones behind large-scale social and activity-feed systems, built from first principles, one architectural decision at a time.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!EaU2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02526676-5f58-433a-9c9d-1a6b6ac572c5_1270x832.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!EaU2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02526676-5f58-433a-9c9d-1a6b6ac572c5_1270x832.png 424w, https://substackcdn.com/image/fetch/$s_!EaU2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02526676-5f58-433a-9c9d-1a6b6ac572c5_1270x832.png 848w, https://substackcdn.com/image/fetch/$s_!EaU2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02526676-5f58-433a-9c9d-1a6b6ac572c5_1270x832.png 1272w, https://substackcdn.com/image/fetch/$s_!EaU2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02526676-5f58-433a-9c9d-1a6b6ac572c5_1270x832.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!EaU2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02526676-5f58-433a-9c9d-1a6b6ac572c5_1270x832.png" width="602" height="394.38110236220473" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/02526676-5f58-433a-9c9d-1a6b6ac572c5_1270x832.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:832,&quot;width&quot;:1270,&quot;resizeWidth&quot;:602,&quot;bytes&quot;:1382037,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://handsonkafka.substack.com/i/207739061?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02526676-5f58-433a-9c9d-1a6b6ac572c5_1270x832.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!EaU2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02526676-5f58-433a-9c9d-1a6b6ac572c5_1270x832.png 424w, https://substackcdn.com/image/fetch/$s_!EaU2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02526676-5f58-433a-9c9d-1a6b6ac572c5_1270x832.png 848w, https://substackcdn.com/image/fetch/$s_!EaU2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02526676-5f58-433a-9c9d-1a6b6ac572c5_1270x832.png 1272w, https://substackcdn.com/image/fetch/$s_!EaU2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02526676-5f58-433a-9c9d-1a6b6ac572c5_1270x832.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Why This Course Matters :</h2><p> Event-driven architecture sits at the center of modern backend systems. Activity feeds, engagement and analytics pipelines, fraud and trust signals, recommendation inputs, and cross-service communication all depend on a messaging backbone that can move enormous volumes of data without losing ordering, duplicating writes, or falling over during a rebalance. As companies move from request-response APIs to event-driven architectures, a naive producer-consumer setup becomes a liability rather than a convenience, and engineers are expected to understand why.</p><h2>Common challenges learners face:</h2><p>Most Java developers know how to call <code>send()</code> on a <code>KafkaTemplate</code>, but far fewer understand what happens when that producer retries under network failure, or when a consumer group rebalances mid-batch. Typical pain points include:</p><ul><li><p>Duplicate or lost messages when producers retry after a transient failure</p></li><li><p>No clear strategy for partition count, keys, or ordering guarantees at scale</p></li><li><p>Consumers that silently drop records during a rebalance or crash mid-commit</p></li><li><p>Schemas that break downstream consumers the moment a field changes</p></li><li><p>Lack of production readiness &#8212; no security, no observability, no plan for connecting Kafka to the rest of the data platform</p></li></ul><h2>Skills addressed by the course :</h2><p>The curriculum directly targets these gaps by teaching producer reliability (idempotence, transactions, acknowledgment strategies, custom partitioning), consumer resilience (manual commits, rebalance listeners, dead letter queues), schema management (JSON Schema, Avro, Protobuf, and Schema Registry compatibility modes), data integration (Kafka Connect, custom connectors, Debezium-based CDC), stream processing (the Kafka Streams DSL and the low-level Processor API), and full production operations (observability, SASL/TLS/ACL security, and a real microservices split).</p><h2>Who Should Enroll</h2><p>This course is designed for a range of backend-focused learners, including:</p><ul><li><p>Java and Spring Boot developers who are comfortable with the basics but have never operated Kafka in production</p></li><li><p>Backend engineers and architects responsible for messaging infrastructure, event pipelines, or real-time data at their organization</p></li><li><p>Students and career changers aiming to build a portfolio project that demonstrates real distributed-systems capability, not just CRUD skills</p></li><li><p>Freelancers and consultants who need to design reliable event-driven systems for client projects</p></li><li><p>Engineering managers and technical leads who want a structured reference for evaluating or reviewing Kafka architecture decisions</p></li><li><p>Anyone preparing for backend or systems-design interviews, where partitioning strategy, delivery guarantees, and exactly-once semantics are frequent topics</p></li><li><p>No prior experience with Kafka is assumed, but comfort with core Java and basic Spring Boot concepts (dependency injection, REST controllers, configuration) is expected going in.</p></li></ul><h2>Learning Outcomes</h2><p>By the end of the course, learners will be able to:</p><ul><li><p>Explain why toy, single-broker Kafka setups fail in production and design around the failure modes</p></li><li><p>Implement idempotent and transactional producers for exactly-once write guarantees across topics</p></li><li><p>Design partition count, key, and custom partitioning strategy for ordering and throughput at a defined scale target</p></li><li><p>Build resilient consumers using manual offset commits, cooperative rebalance listeners, and dead letter topics for poison-pill handling</p></li><li><p>Apply Avro and Protobuf serialization with Confluent Schema Registry, and manage schema evolution under forward, backward, and full compatibility</p></li><li><p>Build custom Kafka Connect source and sink connectors, chain Single Message Transformations, and stream database changes in real time with Debezium</p></li><li><p>Build Kafka Streams topologies &#8212; stateless transformations, windowed aggregations, KTable materialized views, stream-table and table-table joins, interactive queries, and the low-level Processor API</p></li><li><p>Instrument a Kafka-based system with Micrometer, Prometheus, and Grafana, and ship structured logs across every service</p></li><li><p>Secure a cluster end-to-end with SASL/SCRAM authentication, least-privilege ACLs, and cluster-wide TLS</p></li><li><p>Split a single growing codebase into independently deployable microservices that communicate purely through Kafka topics</p></li></ul><h2>Course Curriculum</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tQ15!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26e0dcf5-a7e7-4222-897d-857179fb9f9d_1408x768.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tQ15!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26e0dcf5-a7e7-4222-897d-857179fb9f9d_1408x768.png 424w, https://substackcdn.com/image/fetch/$s_!tQ15!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26e0dcf5-a7e7-4222-897d-857179fb9f9d_1408x768.png 848w, https://substackcdn.com/image/fetch/$s_!tQ15!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26e0dcf5-a7e7-4222-897d-857179fb9f9d_1408x768.png 1272w, https://substackcdn.com/image/fetch/$s_!tQ15!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26e0dcf5-a7e7-4222-897d-857179fb9f9d_1408x768.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tQ15!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26e0dcf5-a7e7-4222-897d-857179fb9f9d_1408x768.png" width="566" height="308.72727272727275" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/26e0dcf5-a7e7-4222-897d-857179fb9f9d_1408x768.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:768,&quot;width&quot;:1408,&quot;resizeWidth&quot;:566,&quot;bytes&quot;:1600247,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://handsonkafka.substack.com/i/207739061?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26e0dcf5-a7e7-4222-897d-857179fb9f9d_1408x768.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tQ15!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26e0dcf5-a7e7-4222-897d-857179fb9f9d_1408x768.png 424w, https://substackcdn.com/image/fetch/$s_!tQ15!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26e0dcf5-a7e7-4222-897d-857179fb9f9d_1408x768.png 848w, https://substackcdn.com/image/fetch/$s_!tQ15!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26e0dcf5-a7e7-4222-897d-857179fb9f9d_1408x768.png 1272w, https://substackcdn.com/image/fetch/$s_!tQ15!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26e0dcf5-a7e7-4222-897d-857179fb9f9d_1408x768.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Module 1: Foundation &amp; Core Concepts (Days 1&#8211;10)</h3><ul><li><p>Day 1: Event-Driven Architecture Fundamentals</p></li><li><p>Day 2: Kafka Cluster Setup</p></li><li><p>Day 3: Topics &amp; Partitions Strategy</p></li><li><p>Day 4: High-Volume Producer Implementation</p></li><li><p>Day 5: Engagement Consumer Development</p></li><li><p>Day 6: Consumer Groups &amp; Scalability</p></li><li><p>Day 7: Offset Management Strategies</p></li><li><p>Day 8: Commit Strategies &amp; Reliability</p></li><li><p>Day 9: Dynamic Consumer Rebalancing</p></li><li><p>Day 10: Delivery Guarantees</p></li></ul><div><hr></div><h3>Module 2: Producer Reliability &amp; Performance (Days 11&#8211;20)</h3><ul><li><p>Day 11: Producer Acknowledgment Strategies</p></li><li><p>Day 12: Retry Logic &amp; Failure Handling</p></li><li><p>Day 13: Idempotent Producers</p></li><li><p>Day 14: Message Ordering &amp; Keys</p></li><li><p>Day 15: Custom Partitioning Logic</p></li><li><p>Day 16: Batching &amp; Throughput Optimization</p></li><li><p>Day 17: Compression Strategies</p></li><li><p>Day 18: Transactional Producers</p></li><li><p>Day 19: Asynchronous Operations &amp; Callbacks</p></li><li><p>Day 20: Replication &amp; ISR Management</p></li></ul><div><hr></div><h3>Module 3: Advanced Consumer Patterns (Days 21&#8211;30)</h3><ul><li><p>Day 21: Manual Partition Assignment</p></li><li><p>Day 22: Low-Latency Consumer Optimization</p></li><li><p>Day 23: Graceful Shutdown Patterns</p></li><li><p>Day 24: Message Headers &amp; Metadata</p></li><li><p>Day 25: Error Handling &amp; Poison Pills</p></li><li><p>Day 26: Schema Definition with JSON Schema</p></li><li><p>Day 27: Avro Serialization &amp; Schema Registry</p></li><li><p>Day 28: Protocol Buffers Integration</p></li><li><p>Day 29: Schema Evolution Strategies</p></li><li><p>Day 30: Log Compaction</p></li></ul><div><hr></div><h3>Module 4: Data Integration &amp; Connect (Days 31&#8211;40)</h3><ul><li><p>Day 31: Kafka Connect Architecture</p></li><li><p>Day 32: Source Connector Implementation</p></li><li><p>Day 33: Sink Connector Development</p></li><li><p>Day 34: Single Message Transformations</p></li><li><p>Day 35: Distributed Connect Deployment</p></li><li><p>Day 36: Connect Monitoring &amp; Observability</p></li><li><p>Day 37: Custom Connector Development</p></li><li><p>Day 38: Change Data Capture with Debezium</p></li><li><p>Day 39: Database Integration Patterns</p></li><li><p>Day 40: Error Handling in Connect</p></li></ul><div><hr></div><h3>Module 5: Stream Processing with Kafka Streams (Days 41&#8211;50)</h3><ul><li><p>Day 41: Kafka Streams Fundamentals</p></li><li><p>Day 42: KStream Processing</p></li><li><p>Day 43: Stateless Transformations</p></li><li><p>Day 44: Stateful Aggregations</p></li><li><p>Day 45: KTable Operations</p></li><li><p>Day 46: Stream-Table Joins</p></li><li><p>Day 47: Table-Table Joins</p></li><li><p>Day 48: Interactive Queries</p></li><li><p>Day 49: Fault Tolerance &amp; Recovery</p></li><li><p>Day 50: Processor API</p></li></ul><div><hr></div><h3>Module 6: Production Operations &amp; Security (Days 51&#8211;60)</h3><ul><li><p>Day 51: Broker Monitoring &amp; Metrics</p></li><li><p>Day 52: Client Metrics &amp; Observability</p></li><li><p>Day 53: Centralized Logging Strategy</p></li><li><p>Day 54: Authentication with SASL</p></li><li><p>Day 55: Authorization with ACLs</p></li><li><p>Day 56: Encryption &amp; TLS</p></li><li><p>Day 57: Microservices Event Architecture</p></li><li><p>Day 58: Schema Governance</p></li><li><p>Day 59: Advanced Ecosystem Tools</p></li><li><p>Day 60: System Integration &amp; Production Readiness</p></li></ul><h2>Practical Applications </h2><p>The concepts in this course map directly onto real engineering work. Producer and consumer reliability patterns apply to any system moving data through a message broker &#8212; not just Kafka, but any queue-backed pipeline that needs to avoid duplication and data loss. The schema-management arc in Module 3 is directly transferable to any service boundary where data contracts need to evolve without breaking consumers. The Connect and CDC patterns in Module 4 are the same techniques used to build real-time data lakes, analytics pipelines, and cross-database synchronization in production systems. The stream-processing work in Module 5 mirrors what real-time analytics and fraud-detection teams build, and the observability and security work in Module 6 is standard practice for operating any Kafka-backed platform in production.</p><p>Taken together, the completed project functions as a credible portfolio piece for backend, platform, or data infrastructure engineering roles, and the individual modules serve as standalone references that can be revisited whenever a related problem &#8212; schema evolution, exactly-once processing, connector development &#8212; comes up in professional work.</p><h2>Conclusion </h2><p>Event-driven systems sit at the intersection of several of the hardest problems in backend engineering &#8212; ordering, delivery guarantees, schema evolution, and operational trust &#8212; and this course was designed to teach all of them through the lens of a single, coherent, incrementally built system. Rather than presenting isolated Kafka features in the abstract, each of the 60 lessons adds a working piece to the same architecture, so that by the end, the concepts of producer reliability, consumer correctness, stream processing, and production security are not just understood but implemented.</p><p>For engineers looking to move beyond basic Kafka familiarity into genuine systems-design capability, this curriculum offers a structured, thorough, and practically grounded path. The full lesson plan and published content are available through Hands On Kafka on Substack, with new lessons rolling out according to the course schedule.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://handsonkafka.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hands On Kafka  is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>