<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Prashant Sasalatti</title>
    <description>The latest articles on DEV Community by Prashant Sasalatti (@prashantsasalatti).</description>
    <link>https://dev.to/prashantsasalatti</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3989425%2F89504f0a-a0f9-4b6c-9d7a-8f912a5fafe0.png</url>
      <title>DEV Community: Prashant Sasalatti</title>
      <link>https://dev.to/prashantsasalatti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prashantsasalatti"/>
    <language>en</language>
    <item>
      <title>The Trade You Can't Close: Why the Hardest Part of Investing Is You</title>
      <dc:creator>Prashant Sasalatti</dc:creator>
      <pubDate>Sat, 20 Jun 2026 09:03:13 +0000</pubDate>
      <link>https://dev.to/prashantsasalatti/the-trade-you-cant-close-why-the-hardest-part-of-investing-is-you-30an</link>
      <guid>https://dev.to/prashantsasalatti/the-trade-you-cant-close-why-the-hardest-part-of-investing-is-you-30an</guid>
      <description>&lt;h3&gt;
  
  
  Why the hardest part of investing isn’t the market. It’s the person making the decisions.
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1611974789855-9c2a0a7236a3%3Fq%3D80%26w%3D1200%26auto%3Dformat%26fit%3Dcrop" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1611974789855-9c2a0a7236a3%3Fq%3D80%26w%3D1200%26auto%3Dformat%26fit%3Dcrop" alt="Candlestick stock chart glowing on a dark screen" width="1200" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by Maxim Hopman on Unsplash&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There is a specific moment every trader knows and almost no one talks about.&lt;/p&gt;

&lt;p&gt;The stock is down. Not catastrophically — just enough. The reason you bought it hasn’t really changed, but the number on the screen has, and now you’re doing something you didn’t plan to do: you’re negotiating. &lt;em&gt;I’ll sell when it gets back to what I paid.&lt;/em&gt; You’re not analyzing anymore. You’re waiting to be made whole, as if the market owes you the price you happened to buy at.&lt;/p&gt;

&lt;p&gt;I have sat in that exact moment more times than I’d like to admit. And the uncomfortable truth I eventually had to accept is that the moment isn’t about the stock at all. It’s about me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern has a name
&lt;/h2&gt;

&lt;p&gt;What I thought was a personal weakness turns out to be one of the most documented behaviors in all of finance. It’s called the &lt;strong&gt;disposition effect&lt;/strong&gt; — the tendency to sell winners too early and hold losers too long.&lt;/p&gt;

&lt;p&gt;It isn’t a hunch. Using account data from a large brokerage, researchers found that individual investors are roughly &lt;strong&gt;50 percent more likely to sell a winning position than a losing one&lt;/strong&gt;, relative to their chances to do so. We lock in gains because they feel good, and we cling to losses because closing them means admitting we were wrong.&lt;/p&gt;

&lt;p&gt;Underneath that sits something older and deeper. Kahneman and Tversky called it &lt;strong&gt;loss aversion&lt;/strong&gt;: the pain of losing is psychologically far heavier than the pleasure of an equal gain. A 10,000 loss doesn’t feel like the mirror image of a 10,000 win. It feels worse — so much worse that we’ll take &lt;em&gt;bigger&lt;/em&gt; risks just to avoid realizing it. That’s why the losing stock is the one you can’t close. Selling it converts a paper loss, which still feels survivable, into a real one, which feels like a verdict.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1560221328-12fe60f83ab8%3Fq%3D80%26w%3D1200%26auto%3Dformat%26fit%3Dcrop" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1560221328-12fe60f83ab8%3Fq%3D80%26w%3D1200%26auto%3Dformat%26fit%3Dcrop" alt="Close-up of a monitor displaying a financial graph" width="1200" height="797"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by Nicholas Cappello on Unsplash&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The expensive part
&lt;/h2&gt;

&lt;p&gt;Here’s the line that rearranged how I think about all of this.&lt;/p&gt;

&lt;p&gt;In a landmark study of more than 66,000 households, the investors who &lt;strong&gt;traded the most earned an annual return of 11.4 percent, while the market returned 17.9 percent.&lt;/strong&gt; Same market. Same years. The only variable was activity. The more they traded, the more they fell behind.&lt;/p&gt;

&lt;p&gt;The cause the researchers settled on wasn’t bad luck or bad information. It was &lt;strong&gt;overconfidence&lt;/strong&gt; — the quiet, constant belief that &lt;em&gt;this time&lt;/em&gt; I’ve read it right, that my conviction is insight rather than noise. Overconfident traders trade more. Trading more costs more and decides worse. The market doesn’t punish stupidity. It punishes certainty.&lt;/p&gt;

&lt;p&gt;And it doesn’t spare any of us by geography. Studies of Indian retail investors find the same fingerprints — loss aversion and mental accounting shaping decisions, emotions riding visibly on the P&amp;amp;L. In one survey, traders strongly agreed that losses significantly hit their mood and confidence, and that their sense of self was tied to whether a trade was green or red. The screen stops being a market and becomes a mirror.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed for me
&lt;/h2&gt;

&lt;p&gt;I didn’t fix this by getting smarter about markets. You can’t out-analyze your own nervous system. What helped was smaller and more boring.&lt;/p&gt;

&lt;p&gt;I started writing the exit &lt;em&gt;before&lt;/em&gt; the entry — the price I’d sell at if I was wrong — when I was still calm, because the version of me holding a loss cannot be trusted to decide. I started treating the urge to “wait until it gets back to even” as a siren, not a strategy; the market has never once cared what I paid. And I started noticing that the trades I felt most certain about were, suspiciously often, the ones that hurt the most.&lt;/p&gt;

&lt;p&gt;The hardest skill in investing isn’t spotting the opportunity. It’s surviving the part where your own brain quietly works against you — and learning to act anyway.&lt;/p&gt;

&lt;p&gt;The trade you can’t close is rarely the problem. It’s usually just the place where you finally see yourself clearly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you’ve ever held a losing position longer than you should have, you already know the feeling I’m describing. I’d love to hear how you learned to let go — or whether you still struggle with it. The comments are open.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nothing here is financial advice. It’s just one person’s attempt to understand why smart people make the same expensive mistakes — myself very much included.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>finance</category>
      <category>psychology</category>
      <category>investing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Deadlock That Only Showed Up Under Load: Debugging DB2 SQLCODE -811 in Production</title>
      <dc:creator>Prashant Sasalatti</dc:creator>
      <pubDate>Sat, 20 Jun 2026 07:08:09 +0000</pubDate>
      <link>https://dev.to/prashantsasalatti/the-deadlock-that-only-showed-up-under-load-debugging-db2-sqlcode-811-in-production-32ol</link>
      <guid>https://dev.to/prashantsasalatti/the-deadlock-that-only-showed-up-under-load-debugging-db2-sqlcode-811-in-production-32ol</guid>
      <description>&lt;p&gt;&lt;em&gt;A real production incident, what the error actually meant, and the fix that stopped it from happening again.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1515879218367-8466d910aaa4%3Fq%3D80%26w%3D1200%26auto%3Dformat%26fit%3Dcrop" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1515879218367-8466d910aaa4%3Fq%3D80%26w%3D1200%26auto%3Dformat%26fit%3Dcrop" alt="Colorful code on a dark screen" width="1200" height="801"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by Chris Ried on Unsplash&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Some bugs are polite. They fail the same way every time, on your machine, with a clean stack trace pointing at the exact line. You fix them before lunch.&lt;/p&gt;

&lt;p&gt;This was not one of those bugs.&lt;/p&gt;

&lt;p&gt;This one only appeared in production, only under concurrent load, and only sometimes. The error code, DB2 &lt;strong&gt;SQLCODE -811&lt;/strong&gt;, is one of those messages that looks like it's telling you something useful while telling you almost nothing. Here's how we tracked it down on a payroll system processing timecards for a large workforce, and what it taught me about writing code that survives concurrency.&lt;/p&gt;
&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;The application is a Java EE enterprise system backed by DB2. During peak processing windows, a batch job that reconciled timecard records started throwing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SQLCODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;811&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SQLSTATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;21000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Intermittently. Not every run. Not reproducible on demand. The kind of failure that makes you stare at logs at 3 AM wondering if you imagined it.&lt;/p&gt;

&lt;p&gt;The job would succeed nine times out of ten. The tenth time, it blew up, and because this was payroll, blowing up one time in ten is not something you get to shrug off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1544197150-b99a580bb7a8%3Fq%3D80%26w%3D1200%26auto%3Dformat%26fit%3Dcrop" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1544197150-b99a580bb7a8%3Fq%3D80%26w%3D1200%26auto%3Dformat%26fit%3Dcrop" alt="Network switch ports with cables plugged into numbered slots" width="1200" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Two paths writing to the same place. Photo by Jordan Harrison on Unsplash.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What SQLCODE -811 actually means
&lt;/h2&gt;

&lt;p&gt;Here's the part the error message hides. SQLCODE -811 is not a deadlock. It's not a lock timeout. It's this: a subquery, embedded SELECT, or SELECT INTO returned &lt;strong&gt;more than one row&lt;/strong&gt; where the code expected exactly one.&lt;/p&gt;

&lt;p&gt;That's it. A scalar context got a non-scalar answer.&lt;/p&gt;

&lt;p&gt;So why was it intermittent? Because the duplicate row only existed transiently, during a window where two processes were both writing to the same logical record before either had committed. Under light load, the timing never lined up. Under heavy concurrent load, it did, and a query that assumed uniqueness suddenly found two rows mid-flight.&lt;/p&gt;

&lt;p&gt;This is the trap with -811: the error points at the SELECT, but the SELECT isn't the bug. The bug is upstream, in how rows got created in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracking it down
&lt;/h2&gt;

&lt;p&gt;Three things made this findable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Stop trusting that it can't return two rows.&lt;/strong&gt; The query had a SELECT INTO against a working table, filtered on what everyone assumed was a unique business key. It wasn't enforced as unique at the database level, it was unique by convention. Convention does not survive a race condition. I added logging to dump the offending key and row count at the moment of failure. The next time it fired, the evidence was unambiguous: two rows, identical business key, created milliseconds apart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Find who writes to that table, and when.&lt;/strong&gt; Two code paths inserted into the same working table. Neither was aware of the other. Both ran during the same processing window. Each one, in isolation, was correct. Together, they produced transient duplicates that a downstream SELECT INTO couldn't handle. This is the thing about concurrency bugs: every individual piece of code can be correct and the system can still be wrong. The defect lives in the gaps between components, not inside any one of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Reproduce it deliberately.&lt;/strong&gt; I could not fix what I could not reproduce. So I forced the race: two threads hammering the same key path concurrently against a test database. Within a few runs, -811 appeared on command. Once a heisenbug becomes reproducible, it stops being scary and becomes ordinary work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;There were three layers to it, in order of how durable each one is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1, enforce uniqueness where it actually lives.&lt;/strong&gt; A unique constraint at the database level. If the data model says a key is unique, the database should be the thing that guarantees it, not the hope that no two processes collide. This converts a silent, intermittent data corruption into a loud, immediate, catchable error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2, make the query defensive.&lt;/strong&gt; Where a SELECT INTO genuinely could encounter more than one row in a valid state, the query needs to say which one it wants, ordering plus a single-row fetch, rather than assuming the result set has exactly one member. Don't ask the database for the row when a row is possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3, fix the write path.&lt;/strong&gt; The real cure: the two code paths needed coordination so they couldn't both create the same logical record in overlapping transactions. Constraints and defensive reads keep you safe; fixing the write path is what makes the problem actually go away.&lt;/p&gt;

&lt;p&gt;The constraint and the defensive read are guardrails. The write-path fix is the cure. You want all three, because guardrails catch the next bug you haven't met yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away from it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The error code points at the crime scene, not the criminal.&lt;/strong&gt; -811 fired in the SELECT, but the SELECT was the victim. The actual bug was an unenforced uniqueness assumption upstream. Read past the line number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unique by convention is a bug with a delay on it.&lt;/strong&gt; Any invariant your code depends on but the database doesn't enforce is a race condition waiting for enough traffic. If it matters, constrain it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Intermittent means timing, and timing means concurrency.&lt;/strong&gt; When a bug refuses to reproduce, stop trying to find the broken line and start looking for two things touching the same state at the same time. The intermittency is the clue, not the obstacle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A heisenbug you can reproduce on demand is already half fixed.&lt;/strong&gt; Most of the work in a concurrency bug is making it deterministic. Once you can summon it, the fix is usually small.&lt;/p&gt;

&lt;p&gt;The satisfying part wasn't the fix. It was that after it shipped, the 3 AM pages for that job stopped completely. The most boring outcome possible, nothing happening, is exactly what you're working toward in production support.&lt;/p&gt;

&lt;p&gt;If you've debugged DB2 under concurrent load, you've probably met -811's cousins: -911 (deadlock/timeout rollback), -803 (duplicate key on a constraint that was enforced), -904 (resource unavailable). They all share the same lesson: the database is telling you the truth about your assumptions, usually at the worst possible hour.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What's the most stubborn intermittent bug you've shipped a fix for? I'd like to hear how you cornered it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>java</category>
      <category>backend</category>
      <category>debugging</category>
    </item>
    <item>
      <title>I Wasted 3 Days on AI Integration Until I Found Spring AI — Here's My Full Spring Boot Chatbot Build</title>
      <dc:creator>Prashant Sasalatti</dc:creator>
      <pubDate>Fri, 19 Jun 2026 07:55:44 +0000</pubDate>
      <link>https://dev.to/prashantsasalatti/i-wasted-3-days-on-ai-integration-until-i-found-spring-ai-heres-my-full-spring-boot-chatbot-build-p4</link>
      <guid>https://dev.to/prashantsasalatti/i-wasted-3-days-on-ai-integration-until-i-found-spring-ai-heres-my-full-spring-boot-chatbot-build-p4</guid>
      <description>&lt;p&gt;Six months ago, I was the guy manually stitching together RestTemplate calls to OpenAI's API, writing my own retry logic, parsing raw JSON responses, and wondering why nobody had made this less painful.&lt;/p&gt;

&lt;p&gt;Then Spring AI 2.0 dropped and I felt genuinely stupid for not waiting.&lt;/p&gt;

&lt;p&gt;This is the exact chatbot I built — from a blank Spring Boot project to a deployed, production-ready AI assistant. No fluff, no "exercise left to the reader." Real code, real mistakes, real gotchas.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Spring AI and Why Does It Matter in 2026?
&lt;/h2&gt;

&lt;p&gt;Spring AI is Spring's official abstraction layer for working with AI models. Think of it the way Spring Data abstracted away JDBC boilerplate — Spring AI does the same for LLM integrations.&lt;/p&gt;

&lt;p&gt;Version 2.0 (released early 2026) brought:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unified &lt;code&gt;ChatClient&lt;/code&gt; API across OpenAI, Anthropic, Ollama, Azure OpenAI, and more&lt;/li&gt;
&lt;li&gt;Built-in streaming support&lt;/li&gt;
&lt;li&gt;First-class RAG (Retrieval-Augmented Generation) support with vector store integrations&lt;/li&gt;
&lt;li&gt;Advisors API for intercepting and augmenting prompts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Java developers, this is huge. You no longer need to learn a Python framework or maintain fragile HTTP wrapper classes. This is proper, idiomatic Spring.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Setup
&lt;/h2&gt;

&lt;p&gt;I used Spring Boot 3.4.x with Java 21. Here's the core &lt;code&gt;pom.xml&lt;/code&gt; section you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;properties&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;java.version&amp;gt;&lt;/span&gt;21&lt;span class="nt"&gt;&amp;lt;/java.version&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;spring-ai.version&amp;gt;&lt;/span&gt;2.0.0&lt;span class="nt"&gt;&amp;lt;/spring-ai.version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/properties&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;dependencyManagement&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.ai&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-ai-bom&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${spring-ai.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;type&amp;gt;&lt;/span&gt;pom&lt;span class="nt"&gt;&amp;lt;/type&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;scope&amp;gt;&lt;/span&gt;import&lt;span class="nt"&gt;&amp;lt;/scope&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependencyManagement&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- Spring Boot Web --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-web&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Spring AI - OpenAI --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.ai&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-ai-openai-spring-boot-starter&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Optional: for streaming responses --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-webflux&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add your API key to &lt;code&gt;application.properties&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;spring.ai.openai.api-key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;${OPENAI_API_KEY}&lt;/span&gt;
&lt;span class="py"&gt;spring.ai.openai.chat.options.model&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;
&lt;span class="py"&gt;spring.ai.openai.chat.options.temperature&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0.7&lt;/span&gt;
&lt;span class="py"&gt;spring.ai.openai.chat.options.max-tokens&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1024&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never hardcode the API key. I know that sounds obvious but I've seen it in 3 different GitHub repos this year.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;Before jumping to code, here's the shape of what we're building:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────┐
│                  CLIENT (Browser/App)           │
└───────────────────┬─────────────────────────────┘
                    │ HTTP POST /api/chat
                    ▼
┌─────────────────────────────────────────────────┐
│              ChatController (REST Layer)        │
│         Validates input, calls service          │
└───────────────────┬─────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────┐
│             ChatService (Business Logic)        │
│   Builds prompt, injects system context,        │
│   handles errors, logs usage                    │
└───────────────────┬─────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────┐
│            Spring AI ChatClient                 │
│   Abstracts the LLM provider (OpenAI here)      │
└───────────────────┬─────────────────────────────┘
                    │
                    ▼
              OpenAI API (gpt-4o)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple. Clean. Swappable — if tomorrow you want to run Ollama locally, you change one dependency and one properties line.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building the Chatbot — Step by Step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Service Layer
&lt;/h3&gt;

&lt;p&gt;This is where the real logic lives. I kept the controller thin and put everything meaningful here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="nd"&gt;@Slf4j&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""
        You are a helpful assistant for a Java developer support portal.
        Be concise, technical, and accurate.
        If you don't know something, say so — don't hallucinate.
        Always format code snippets using markdown.
        """&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ChatService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultSystem&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sending message to AI. Length: {} chars"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Response received. Length: {} chars"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AI call failed: {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ChatServiceException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unable to process your request. Please try again."&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ChatClient.Builder&lt;/code&gt; is auto-configured by Spring AI — you just inject it. The &lt;code&gt;defaultSystem()&lt;/code&gt; call sets a persistent system prompt for every conversation. This is your personality config for the bot.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Controller
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/chat"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Validated&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ChatService&lt;/span&gt; &lt;span class="n"&gt;chatService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ChatController&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatService&lt;/span&gt; &lt;span class="n"&gt;chatService&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@PostMapping&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChatResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestBody&lt;/span&gt; &lt;span class="nd"&gt;@Valid&lt;/span&gt; &lt;span class="nc"&gt;ChatRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ChatResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;ChatRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nd"&gt;@NotBlank&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Message cannot be blank"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="nd"&gt;@Size&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Message too long"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;ChatResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Records for DTOs. Validation annotations on input. No boilerplate. This is what Java 21 + Spring Boot 3 feels like when it's working well.&lt;/p&gt;




&lt;h2&gt;
  
  
  Adding Conversation Memory
&lt;/h2&gt;

&lt;p&gt;The above is stateless — every call is a fresh context. For a real chatbot, you need memory. Spring AI handles this with the &lt;code&gt;MessageChatMemoryAdvisor&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="nd"&gt;@Slf4j&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StatefulChatService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;InMemoryChatMemory&lt;/span&gt; &lt;span class="n"&gt;chatMemory&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;StatefulChatService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatMemory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InMemoryChatMemory&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultSystem&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"You are a helpful Java developer assistant."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultAdvisors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MessageChatMemoryAdvisor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatMemory&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;conversationId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;advisors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;advisor&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;advisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;AbstractChatMemoryAdvisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CHAT_MEMORY_CONVERSATION_ID_KEY&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;conversationId&lt;/span&gt;
            &lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass a &lt;code&gt;conversationId&lt;/code&gt; (like a session ID or user ID) and Spring AI automatically maintains the message history for that conversation. For production, swap &lt;code&gt;InMemoryChatMemory&lt;/code&gt; with a Redis or database-backed implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Considerations
&lt;/h2&gt;

&lt;p&gt;This is the part most tutorials skip. Here's what bit me in staging:&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate Limiting
&lt;/h3&gt;

&lt;p&gt;OpenAI has TPM (tokens per minute) limits. Under load, you will hit them. Add a simple token bucket or use Resilience4j:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;RateLimiter&lt;/span&gt; &lt;span class="nf"&gt;aiRateLimiter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RateLimiterRegistry&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rateLimiter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"openai"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RateLimiterConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;custom&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;limitForPeriod&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;limitRefreshPeriod&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofMinutes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timeoutDuration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrap your &lt;code&gt;chatService.chat()&lt;/code&gt; call with &lt;code&gt;RateLimiter.decorateSupplier(...)&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeouts
&lt;/h3&gt;

&lt;p&gt;OpenAI's API can be slow — especially under load. Set explicit timeouts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;spring.ai.openai.chat.options.timeout&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And handle &lt;code&gt;TimeoutException&lt;/code&gt; separately from other errors so you can return a user-friendly "the AI is taking too long, try again" message instead of a generic 500.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logging Token Usage
&lt;/h3&gt;

&lt;p&gt;You'll want to track costs. Use Spring AI's response metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ChatResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatResponse&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;Usage&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMetadata&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getUsage&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tokens used — prompt: {}, completion: {}, total: {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPromptTokens&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getGenerationTokens&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTotalTokens&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ship these metrics to your observability stack. Untracked AI costs will surprise you at billing time.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Pro Tip:&lt;/strong&gt; Use Spring AI's &lt;code&gt;Advisor&lt;/code&gt; pattern to implement cross-cutting concerns like content filtering, PII detection, and audit logging — without polluting your service layer. Think of Advisors like AOP for your AI calls.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Deployment Tips
&lt;/h2&gt;

&lt;p&gt;I deployed this on a basic OpenShift container (which is our standard at work). A few things worth noting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment variables over properties files.&lt;/strong&gt; Set &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; as a secret, reference it with &lt;code&gt;${OPENAI_API_KEY}&lt;/code&gt; in properties. Never bake the key into your image.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Health checks matter.&lt;/strong&gt; Add a custom health indicator that pings the AI provider on startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AiHealthIndicator&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;HealthIndicator&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Health&lt;/span&gt; &lt;span class="nf"&gt;health&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ping"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Health&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;up&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;withDetail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"provider"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"OpenAI"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Health&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;down&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;withDetail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Horizontal scaling is fine&lt;/strong&gt; — since AI state lives in the LLM provider, your app is stateless (unless you're using in-memory chat history, in which case: pin sessions or use Redis).&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistakes I Made (So You Don't Have To)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Forgetting to set max-tokens.&lt;/strong&gt;&lt;br&gt;
Without a limit, the model will generate until it's done. Long responses = high cost = slow responses. Always set &lt;code&gt;max-tokens&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Putting too much in the system prompt.&lt;/strong&gt;&lt;br&gt;
I crammed business logic into a 500-word system prompt. It made outputs inconsistent. Keep system prompts focused and short — under 200 words ideally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Not handling empty/null responses.&lt;/strong&gt;&lt;br&gt;
Spring AI's &lt;code&gt;.content()&lt;/code&gt; can return null if the model returns a stop reason other than "stop" (like content filtering). Add a null check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Skipping integration tests.&lt;/strong&gt;&lt;br&gt;
Unit tests with mocked &lt;code&gt;ChatClient&lt;/code&gt; are fine, but you need at least a few integration tests that hit the real API. Model behavior changes between versions. I found a regression this way before it hit production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Using blocking calls in a WebFlux context.&lt;/strong&gt;&lt;br&gt;
If you're mixing Spring AI's blocking &lt;code&gt;call()&lt;/code&gt; API with reactive code, you'll get thread starvation. Either go fully reactive (use &lt;code&gt;.stream()&lt;/code&gt; and return &lt;code&gt;Flux&amp;lt;String&amp;gt;&lt;/code&gt;) or keep it on a dedicated thread pool.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Spring AI 2.0 makes LLM integration feel like writing any other Spring service — idiomatic, testable, and provider-agnostic&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ChatClient&lt;/code&gt; builder pattern is your main API surface — learn it well&lt;/li&gt;
&lt;li&gt;Memory management (&lt;code&gt;MessageChatMemoryAdvisor&lt;/code&gt;) is built-in — don't hand-roll it&lt;/li&gt;
&lt;li&gt;Production readiness means: rate limiting, timeouts, token tracking, and proper health checks&lt;/li&gt;
&lt;li&gt;The Advisors API is underrated — use it for cross-cutting AI concerns&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;I'm currently adding RAG to this project — embedding our internal documentation into a vector store (PGVector on PostgreSQL) and letting the chatbot answer questions grounded in actual docs rather than hallucinating.&lt;/p&gt;

&lt;p&gt;After that: streaming responses with SSE so the UI feels instant instead of waiting for the full completion.&lt;/p&gt;

&lt;p&gt;If this was useful, follow me here on DEV — I'll be posting the RAG follow-up next week.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Have you integrated AI into a Spring Boot project? What was the hardest part — the setup, the prompting, or convincing your team it was production-safe? Drop it in the comments. 👇&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Integrate Apache Kafka with Spring Boot: A Production-Ready Guide</title>
      <dc:creator>Prashant Sasalatti</dc:creator>
      <pubDate>Thu, 18 Jun 2026 15:21:15 +0000</pubDate>
      <link>https://dev.to/prashantsasalatti/how-to-integrate-apache-kafka-with-spring-boot-a-production-ready-guide-e6d</link>
      <guid>https://dev.to/prashantsasalatti/how-to-integrate-apache-kafka-with-spring-boot-a-production-ready-guide-e6d</guid>
      <description>&lt;p&gt;When a Spring Boot service needs to talk to another service without waiting on a synchronous HTTP call, message queues are the usual answer. Apache Kafka has become the default choice for this in most backend teams, but a lot of tutorials stop at a "hello world" producer and consumer that would never survive a real production load. Things like consumer retries, error handling, serialization of real objects, and graceful shutdown get skipped, and those are exactly the parts that page you at 2 a.m.&lt;/p&gt;

&lt;p&gt;In this tutorial, you will build a Spring Boot application that produces and consumes JSON messages over Kafka. You will configure a producer and a consumer, send a typed object instead of a plain string, handle deserialization errors so one bad message does not block your whole consumer group, and verify the whole thing works end to end. By the end, you will have a small but realistic messaging setup you can build on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To follow along, you will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Java 17 or later installed. You can check your version by running &lt;code&gt;java -version&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A Spring Boot 3.x project. You can generate one at &lt;a href="https://start.spring.io" rel="noopener noreferrer"&gt;start.spring.io&lt;/a&gt; with the &lt;strong&gt;Spring for Apache Kafka&lt;/strong&gt; dependency added.&lt;/li&gt;
&lt;li&gt;A running Kafka broker. The quickest way to get one locally is Docker, which the first step covers.&lt;/li&gt;
&lt;li&gt;Basic familiarity with Spring Boot, including how &lt;code&gt;@Component&lt;/code&gt; and &lt;code&gt;application.yml&lt;/code&gt; work.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1 — Running Kafka Locally with Docker
&lt;/h2&gt;

&lt;p&gt;Before writing any code, you need a broker to talk to. Running Kafka by hand involves Zookeeper, broker configuration, and a fair amount of setup, so you will use Docker Compose to bring up a single-broker cluster instead.&lt;/p&gt;

&lt;p&gt;Create a file named &lt;code&gt;docker-compose.yml&lt;/code&gt; in your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kafka&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apache/kafka:3.7.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kafka&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9092:9092"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;KAFKA_NODE_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;KAFKA_PROCESS_ROLES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;broker,controller&lt;/span&gt;
      &lt;span class="na"&gt;KAFKA_LISTENERS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PLAINTEXT://:9092,CONTROLLER://:9093&lt;/span&gt;
      &lt;span class="na"&gt;KAFKA_ADVERTISED_LISTENERS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PLAINTEXT://localhost:9092&lt;/span&gt;
      &lt;span class="na"&gt;KAFKA_CONTROLLER_LISTENER_NAMES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CONTROLLER&lt;/span&gt;
      &lt;span class="na"&gt;KAFKA_LISTENER_SECURITY_PROTOCOL_MAP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT&lt;/span&gt;
      &lt;span class="na"&gt;KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9093      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This uses Kafka's KRaft mode, which removes the Zookeeper dependency that older setups required. The &lt;code&gt;KAFKA_ADVERTISED_LISTENERS&lt;/code&gt; value tells clients how to reach the broker; setting it to &lt;code&gt;localhost:9092&lt;/code&gt; is what lets your Spring Boot app connect from your machine.&lt;/p&gt;

&lt;p&gt;Start the broker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm it is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the &lt;code&gt;kafka&lt;/code&gt; container with a state of &lt;code&gt;running&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With a broker available, you can now configure your application to connect to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Configuring the Producer and Consumer
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;src/main/resources/application.yml&lt;/code&gt; and add the Kafka configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kafka&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;bootstrap-servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost:9092&lt;/span&gt;
    &lt;span class="na"&gt;producer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;key-serializer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;org.apache.kafka.common.serialization.StringSerializer&lt;/span&gt;
      &lt;span class="na"&gt;value-serializer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;org.springframework.kafka.support.serializer.JsonSerializer&lt;/span&gt;
    &lt;span class="na"&gt;consumer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;group-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders-service&lt;/span&gt;
      &lt;span class="na"&gt;auto-offset-reset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;earliest&lt;/span&gt;
      &lt;span class="na"&gt;key-deserializer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;org.apache.kafka.common.serialization.StringDeserializer&lt;/span&gt;
      &lt;span class="na"&gt;value-deserializer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;org.springframework.kafka.support.serializer.ErrorHandlingDeserializer&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;spring.deserializer.value.delegate.class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;org.springframework.kafka.support.serializer.JsonDeserializer&lt;/span&gt;
        &lt;span class="na"&gt;spring.json.trusted.packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;com.example.kafka.model"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few of these settings are worth calling out, because they are the difference between a demo and something production-ready:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;JsonSerializer&lt;/code&gt; and &lt;code&gt;JsonDeserializer&lt;/code&gt; let you send and receive Java objects as JSON rather than hand-serializing strings.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;auto-offset-reset: earliest&lt;/code&gt; means a new consumer group reads from the beginning of the topic rather than skipping existing messages, which is what you usually want during development.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ErrorHandlingDeserializer&lt;/code&gt; wraps the JSON deserializer. This is the important one. Without it, a single malformed message can throw a deserialization exception that the consumer cannot get past, and it will retry that same poisoned message forever, blocking every message behind it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;spring.json.trusted.packages&lt;/code&gt; restricts which packages the deserializer will instantiate, which prevents a class of deserialization security issues. Set it to the package where your message classes live.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3 — Defining the Message Model
&lt;/h2&gt;

&lt;p&gt;Create a simple record to represent the message payload. In a package called &lt;code&gt;com.example.kafka.model&lt;/code&gt;, create &lt;code&gt;OrderEvent.java&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example.kafka.model&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;OrderEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using a &lt;code&gt;record&lt;/code&gt; keeps the model immutable and removes boilerplate. Because the package matches the &lt;code&gt;spring.json.trusted.packages&lt;/code&gt; value from Step 2, the consumer will be allowed to deserialize incoming messages into this type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Writing the Producer
&lt;/h2&gt;

&lt;p&gt;Now create the producer that sends &lt;code&gt;OrderEvent&lt;/code&gt; messages to a topic. In &lt;code&gt;com.example.kafka&lt;/code&gt;, create &lt;code&gt;OrderProducer.java&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example.kafka&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.example.kafka.model.OrderEvent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.kafka.core.KafkaTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.stereotype.Service&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderProducer&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;TOPIC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;KafkaTemplate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OrderEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;kafkaTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderProducer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;KafkaTemplate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OrderEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;kafkaTemplate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;kafkaTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kafkaTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;kafkaTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TOPIC&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;whenComplete&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sent order "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" to partition "&lt;/span&gt;
                        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRecordMetadata&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;partition&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to send order "&lt;/span&gt;
                        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;": "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;});&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;KafkaTemplate&lt;/code&gt; is the Spring abstraction for producing messages, and Spring Boot autoconfigures it from the properties you set in Step 2. Two details matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Passing &lt;code&gt;event.orderId()&lt;/code&gt; as the message key means all events for the same order land on the same partition, which preserves ordering for that order.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;send()&lt;/code&gt; returns a &lt;code&gt;CompletableFuture&lt;/code&gt;. Attaching a &lt;code&gt;whenComplete&lt;/code&gt; callback lets you log delivery success or failure instead of firing the message and hoping it arrived. In production you would replace these &lt;code&gt;println&lt;/code&gt; calls with a proper logger and metrics.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5 — Writing the Consumer with Error Handling
&lt;/h2&gt;

&lt;p&gt;Create the consumer that listens to the &lt;code&gt;orders&lt;/code&gt; topic. In &lt;code&gt;com.example.kafka&lt;/code&gt;, create &lt;code&gt;OrderConsumer.java&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example.kafka&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.example.kafka.model.OrderEvent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.kafka.annotation.KafkaListener&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.stereotype.Component&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderConsumer&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@KafkaListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groupId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orders-service"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Received order "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" from "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" for $"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="c1"&gt;// business logic goes here&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@KafkaListener&lt;/code&gt; annotation does the heavy lifting: Spring polls the topic, deserializes each message into an &lt;code&gt;OrderEvent&lt;/code&gt;, and calls this method for every record.&lt;/p&gt;

&lt;p&gt;The error handling you set up in Step 2 is what makes this safe. Because the consumer uses &lt;code&gt;ErrorHandlingDeserializer&lt;/code&gt;, a message that cannot be parsed into an &lt;code&gt;OrderEvent&lt;/code&gt; is handed off as a failed record instead of crashing the listener. By default, Spring's error handler will retry a few times and then log and skip the bad record, so one corrupt message does not stall the entire partition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 — Verifying It Works End to End
&lt;/h2&gt;

&lt;p&gt;Wire the producer to a startup runner so you can see a message flow through. In your main application class, add a bean that sends a test event once the context is ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example.kafka&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.example.kafka.model.OrderEvent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.boot.CommandLineRunner&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.boot.SpringApplication&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.boot.autoconfigure.SpringBootApplication&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.context.annotation.Bean&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@SpringBootApplication&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KafkaApplication&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SpringApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;KafkaApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;    &lt;span class="nc"&gt;CommandLineRunner&lt;/span&gt; &lt;span class="nf"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderProducer&lt;/span&gt; &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Acme Corp"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;249.99&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./mvnw spring-boot:run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the logs, you should see the producer confirm delivery and the consumer report receipt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sent order ORD-1001 to partition 0
Received order ORD-1001 from Acme Corp for $249.99
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seeing both lines means the message was serialized to JSON, published to the broker, pulled by the consumer group, deserialized back into an &lt;code&gt;OrderEvent&lt;/code&gt;, and processed. The full round trip is working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this tutorial, you set up a local Kafka broker with Docker, configured a Spring Boot producer and consumer for typed JSON messages, and added deserialization error handling so a single bad message cannot block your consumer group. You also keyed messages by order ID to preserve per-order ordering and logged delivery results instead of sending blindly.&lt;/p&gt;

&lt;p&gt;From here, you can extend this setup in several directions. You can route repeatedly failing messages to a dead letter topic so they can be inspected later, add a &lt;code&gt;RetryTopic&lt;/code&gt; configuration for delayed retries, or introduce a schema registry with Avro if you need stronger contracts between producers and consumers.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>kafka</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
