<?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: Praveen Yadav</title>
    <description>The latest articles on DEV Community by Praveen Yadav (@ykpraveen).</description>
    <link>https://dev.to/ykpraveen</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%2F569972%2F089d56a9-b0f6-4d91-ba28-0b5cb92248cd.png</url>
      <title>DEV Community: Praveen Yadav</title>
      <link>https://dev.to/ykpraveen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ykpraveen"/>
    <language>en</language>
    <item>
      <title>Building Distributed Data Processing with Spring Batch 6 + Spring Boot 4</title>
      <dc:creator>Praveen Yadav</dc:creator>
      <pubDate>Thu, 25 Jun 2026 11:37:27 +0000</pubDate>
      <link>https://dev.to/ykpraveen/building-distributed-data-processing-with-spring-batch-6-spring-boot-4-5hf8</link>
      <guid>https://dev.to/ykpraveen/building-distributed-data-processing-with-spring-batch-6-spring-boot-4-5hf8</guid>
      <description>&lt;p&gt;When people first use Spring Batch, they usually start with a simple single-threaded job. That works for small datasets, but once data volume grows, throughput becomes the bottleneck.&lt;/p&gt;

&lt;p&gt;In this sample project, I implemented a &lt;strong&gt;partitioned, multi-threaded Spring Batch pipeline&lt;/strong&gt; to process sales records in parallel using a master/worker step model.&lt;/p&gt;

&lt;p&gt;👉 Code repo: &lt;a href="https://github.com/ykpraveen/spring-batch-sample" rel="noopener noreferrer"&gt;github.com/ykpraveen/spring-batch-sample&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Spring Batch
&lt;/h2&gt;

&lt;p&gt;At its core, Spring Batch is built around a few key abstractions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Job&lt;/strong&gt;: a complete batch workflow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step&lt;/strong&gt;: one phase of a job&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ItemReader / ItemProcessor / ItemWriter&lt;/strong&gt;: read-transform-write pipeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunk processing&lt;/strong&gt;: process N items in one transaction (&lt;code&gt;chunkSize&lt;/code&gt;)
### Why chunking matters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In chunk-oriented steps, Spring Batch reads and processes items until the chunk size is reached, then writes and commits in one transaction.&lt;/p&gt;

&lt;p&gt;So with &lt;code&gt;chunk(500)&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;500 items are read/processed/written&lt;/li&gt;
&lt;li&gt;one commit happens per chunk&lt;/li&gt;
&lt;li&gt;failures can be retried at chunk boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives a good balance between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;too-small chunks (high transaction overhead)&lt;/li&gt;
&lt;li&gt;too-large chunks (long transactions, higher rollback cost)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How Spring Batch scales
&lt;/h3&gt;

&lt;p&gt;Spring Batch offers multiple scaling patterns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Multi-threaded Step&lt;/strong&gt;: one step, concurrent chunk processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partitioning&lt;/strong&gt;: split input domain into partitions, each handled by a worker step&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote Chunking / Remote Partitioning&lt;/strong&gt;: distribute work across processes/nodes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This project uses &lt;strong&gt;partitioning + thread pool execution&lt;/strong&gt; (local distributed-style parallelism).&lt;/p&gt;

&lt;h2&gt;
  
  
  How this project applies those concepts
&lt;/h2&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/ykpraveen/spring-batch-sample" rel="noopener noreferrer"&gt;spring-batch-sample&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The architecture is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;master step&lt;/strong&gt; creates partitions (data ranges)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;worker step&lt;/strong&gt; executes each partition&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;ThreadPoolTaskExecutor&lt;/code&gt; runs workers concurrently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key classes (see &lt;code&gt;src/main/java&lt;/code&gt; in repo):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;BatchConfiguration&lt;/code&gt; → job/step orchestration&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SalesDataPartitioner&lt;/code&gt; → partition boundary logic&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SalesDataProcessor&lt;/code&gt; → business transformation logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code area: &lt;a href="https://github.com/ykpraveen/spring-batch-sample/tree/main/src/main/java" rel="noopener noreferrer"&gt;src/main/java&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance tuning used here
&lt;/h2&gt;

&lt;p&gt;The sample uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gridSize: 8&lt;/code&gt; (number of partitions)&lt;/li&gt;
&lt;li&gt;Thread pool: &lt;code&gt;corePoolSize=4&lt;/code&gt;, &lt;code&gt;maxPoolSize=8&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chunk size: 500&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Sample input: 5000 records&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Interpretation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gridSize&lt;/code&gt; controls parallel work units.&lt;/li&gt;
&lt;li&gt;Thread pool size controls actual concurrent execution.&lt;/li&gt;
&lt;li&gt;Effective throughput depends on DB I/O, CPU, and item processing complexity.&lt;/li&gt;
&lt;li&gt;Increasing partitions beyond available threads can still help load balancing, but with diminishing returns.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Database + metadata angle
&lt;/h2&gt;

&lt;p&gt;Spring Batch is not just a processing framework; it is also a &lt;strong&gt;stateful execution framework&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It tracks job/step execution state in metadata, enabling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;restartability&lt;/li&gt;
&lt;li&gt;execution history&lt;/li&gt;
&lt;li&gt;failure diagnostics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this sample, PostgreSQL stores both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;domain tables (&lt;code&gt;sales_data&lt;/code&gt;, &lt;code&gt;processed_data&lt;/code&gt;, &lt;code&gt;processing_statistics&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;batch execution context/metadata managed by Spring Batch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination is what makes batch jobs operationally reliable in real systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Run locally
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Start PostgreSQL
&lt;/h3&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;h3&gt;
  
  
  2) Build and run the app
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn clean &lt;span class="nb"&gt;install
&lt;/span&gt;mvn spring-boot:run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3) Trigger the batch job
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/api/batch/start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4) Stop PostgreSQL
&lt;/h3&gt;



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

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why this pattern is useful in real projects
&lt;/h2&gt;

&lt;p&gt;This design is a strong baseline for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ETL and data migration&lt;/li&gt;
&lt;li&gt;order/payment reconciliation&lt;/li&gt;
&lt;li&gt;large-volume reporting prep&lt;/li&gt;
&lt;li&gt;scheduled backend data shaping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clear separation of orchestration vs business logic&lt;/li&gt;
&lt;li&gt;predictable transactional boundaries&lt;/li&gt;
&lt;li&gt;scalable parallel execution&lt;/li&gt;
&lt;li&gt;operational observability through batch metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next extensions
&lt;/h2&gt;

&lt;p&gt;If you want to evolve this sample toward production-grade scale:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add retry/skip policies for fault tolerance.&lt;/li&gt;
&lt;li&gt;Export job metrics (Micrometer + Prometheus/Grafana).&lt;/li&gt;
&lt;li&gt;Make partition strategy adaptive to dataset size.&lt;/li&gt;
&lt;li&gt;Move to remote partitioning for multi-node execution.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;If you’re learning Spring Batch or designing high-throughput processing pipelines, this pattern is a solid starting point: simple enough to understand, realistic enough to extend.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>batch</category>
      <category>java</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Building an AI Chat Agent with MCP, Spring AI</title>
      <dc:creator>Praveen Yadav</dc:creator>
      <pubDate>Wed, 24 Jun 2026 09:41:20 +0000</pubDate>
      <link>https://dev.to/ykpraveen/building-an-ai-chat-agent-with-mcp-spring-ai-f0n</link>
      <guid>https://dev.to/ykpraveen/building-an-ai-chat-agent-with-mcp-spring-ai-f0n</guid>
      <description>&lt;p&gt;Model Context Protocol (MCP) is an open standard for connecting AI apps to tools and data sources. A useful way to think about it is as a USB-C port for AI: one standard interface that lets different models plug into different capabilities without custom glue code for every integration.&lt;/p&gt;

&lt;p&gt;In this project, we combine MCP, Spring AI, and Google Gemini to build a chat app that can answer weather questions using real tools instead of hallucinating. The system has three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP tool server&lt;/strong&gt; - a Spring Boot service that exposes weather and geocoding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI chat agent&lt;/strong&gt; - a Spring Boot service that uses Spring AI + Gemini and calls MCP tools when needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React chat UI&lt;/strong&gt; - a lightweight frontend for sending messages and rendering replies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a small but realistic architecture you can extend into a production assistant.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User (Browser:3000)
    | POST /api/chat
    v
AI Agent (Spring:7171) -- MCP / Streamable HTTP --&amp;gt; MCP Server (Spring:7170)
    |                                               |
    | Google Gemini                                 | Bright Sky API (weather)
    |                                               | OpenStreetMap Nominatim (geocoding)
    v                                               v
Chat response                                    Tool execution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full source code is available on &lt;a href="https://github.com/ykpraveen/mcp-spring-sample" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The MCP Tool Server
&lt;/h2&gt;

&lt;p&gt;The tool server is a Spring Boot application that exposes MCP tools through Spring AI's annotation scanner. It runs on port &lt;code&gt;7170&lt;/code&gt; and uses Streamable HTTP for transport.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependencies
&lt;/h3&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;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-starter-mcp-server-webmvc&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Defining tools
&lt;/h3&gt;

&lt;p&gt;With Spring AI, a tool is just a Spring bean method annotated with &lt;code&gt;@McpTool&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;@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;WeatherTool&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;WeatherToolService&lt;/span&gt; &lt;span class="n"&gt;weatherToolService&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;WeatherTool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WeatherToolService&lt;/span&gt; &lt;span class="n"&gt;weatherToolService&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;weatherToolService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;weatherToolService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@McpTool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"get_current_weather"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Get current weather by dwd_station_id or by lat/lon"&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;Map&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;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getCurrentWeather&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nd"&gt;@McpToolParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"DWD station ID"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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;dwd_station_id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nd"&gt;@McpToolParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Latitude"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nd"&gt;@McpToolParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Longitude"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt; &lt;span class="n"&gt;lon&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;weatherToolService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getWeather&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dwd_station_id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon&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;Spring turns that method into an MCP tool definition and publishes the parameter metadata as part of the schema. That means the model can discover the tool, understand its inputs, and decide when to call it.&lt;/p&gt;

&lt;p&gt;The project also includes a geocoding tool that resolves city names to coordinates:&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;@McpTool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"geocode_city"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Convert a city name to latitude and longitude using OpenStreetMap Nominatim"&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;Map&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;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;geocodeCity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nd"&gt;@McpToolParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"City name (e.g., 'Berlin', 'New York')"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;cityName&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;h3&gt;
  
  
  The service layer
&lt;/h3&gt;

&lt;p&gt;The tools delegate the real work to services that handle validation, caching, and external API calls:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WeatherToolService&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;Map&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;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getWeather&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;dwdStationId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Validate the request&lt;/span&gt;
        &lt;span class="c1"&gt;// Check the cache&lt;/span&gt;
        &lt;span class="c1"&gt;// Call Bright Sky if needed&lt;/span&gt;
        &lt;span class="c1"&gt;// Return a structured response&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 key design choices are straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Separate TTL caches&lt;/strong&gt; for station-id and coordinate lookups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured responses&lt;/strong&gt; with &lt;code&gt;success&lt;/code&gt;, &lt;code&gt;error_code&lt;/code&gt;, and &lt;code&gt;error_message&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache metadata&lt;/strong&gt; in each response so you can see whether the result came from cache or upstream&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Server configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7170&lt;/span&gt;

&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mcp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spring-sample-mcp-server&lt;/span&gt;
        &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.0.0&lt;/span&gt;
        &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;STREAMABLE&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SYNC&lt;/span&gt;
        &lt;span class="na"&gt;annotation-scanner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;mcp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MCP_API_KEY:}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;STREAMABLE&lt;/code&gt; protocol gives the agent a lightweight MCP transport, and the shared API key keeps the demo simple without adding full auth infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Security for the Demo
&lt;/h2&gt;

&lt;p&gt;The MCP server and agent share an &lt;code&gt;MCP_API_KEY&lt;/code&gt;. The agent adds it automatically as an &lt;code&gt;X-API-Key&lt;/code&gt; header, and the server validates it on inbound MCP requests.&lt;/p&gt;

&lt;p&gt;That is enough for local development and a sample project. For anything public-facing, move to Spring Security, OAuth2 or JWT, rate limiting, and a gateway in front of the MCP endpoint.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The AI Chat Agent
&lt;/h2&gt;

&lt;p&gt;The agent is responsible for deciding when to use tools, calling Gemini, and keeping the conversation stateful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependencies
&lt;/h3&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;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-starter-model-google-genai&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;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-starter-mcp-client&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  MCP client configuration
&lt;/h3&gt;

&lt;p&gt;The agent injects the shared API key through a custom HTTP request customizer:&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;@Configuration&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;AgentConfiguration&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;McpClientCustomizer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HttpClientStreamableHttpTransport&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;streamableHttpTransportCustomizer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AgentProperties&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;McpSyncHttpClientRequestCustomizer&lt;/span&gt; &lt;span class="n"&gt;requestCustomizer&lt;/span&gt; &lt;span class="o"&gt;=&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="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&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="nc"&gt;StringUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMcpApiKey&lt;/span&gt;&lt;span class="o"&gt;()))&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;header&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-API-Key"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMcpApiKey&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="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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="o"&gt;-&amp;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;httpRequestCustomizer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestCustomizer&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;h3&gt;
  
  
  Core chat flow
&lt;/h3&gt;

&lt;p&gt;The agent keeps a small in-memory conversation history, checks whether the user message looks like a tool request, and then routes the prompt through either a plain Gemini client or a tool-enabled client.&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="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;reply&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;sessionId&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="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ConversationTurn&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memoryStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;history&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&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;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buildPrompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;history&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="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;toolRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shouldUseTools&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="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toolRequest&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;toolEnabledClient&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;plainChatClient&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;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;invokeModel&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;memoryStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appendTurn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&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="n"&gt;answer&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;answer&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 lazy initialization is deliberate: the agent can start even if the MCP server is down, and it only initializes MCP clients when a tool request actually arrives.&lt;/p&gt;

&lt;p&gt;The tool trigger is intentionally simple:&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="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;shouldUseTools&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="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;normalized&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;toLowerCase&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Locale&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ROOT&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&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;keyword&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;TOOL_KEYWORDS&lt;/span&gt;&lt;span class="o"&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;normalized&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&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="kc"&gt;true&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;That heuristic is enough for a demo and easy to explain. In a larger system, you could replace it with a router model or intent classifier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Virtual threads and timeout handling
&lt;/h3&gt;

&lt;p&gt;The model call runs on a virtual thread with a configurable timeout so the request does not hang forever if Gemini is slow or unreachable:&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="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;invokeModel&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="n"&gt;client&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;prompt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Executors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newVirtualThreadPerTaskExecutor&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;client&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="n"&gt;prompt&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="n"&gt;future&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SECONDS&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;TimeoutException&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ResponseStatusException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;GATEWAY_TIMEOUT&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="k"&gt;finally&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;shutdownNow&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;h3&gt;
  
  
  Session memory
&lt;/h3&gt;

&lt;p&gt;Conversation history lives in an in-memory LRU store with a small per-session turn window. That keeps follow-up questions like "What about tomorrow?" grounded in the earlier exchange without introducing a database too early.&lt;/p&gt;

&lt;p&gt;The agent configuration sets the model to &lt;code&gt;gemini-3.5-flash&lt;/code&gt;, the memory limit to 20 turns per session, and the session cap to 500.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The React Chat UI
&lt;/h2&gt;

&lt;p&gt;The frontend is a Vite app with a simple chat window, minimal state, and no component library.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}]);&lt;/span&gt;
    &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}]);&lt;/span&gt;
    &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Vite dev server proxies &lt;code&gt;/api/*&lt;/code&gt; to the agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:7171&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;changeOrigin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;rewrite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;api/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI is intentionally plain: a purple gradient, responsive layout, and a smooth message list are enough to make the app feel complete without distracting from the architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Putting It All Together
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Running the application
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Set the environment variables:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_gemini_api_key
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MCP_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;a_shared_secret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Start the MCP server:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;mcp-server-spring
mvn spring-boot:run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Start the agent:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;mcp-spring-agent
mvn spring-boot:run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Start the UI:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;mcp-ui
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What happens when you ask a question
&lt;/h3&gt;

&lt;p&gt;If the user asks, "What's the weather in Berlin?" the flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The agent sees the word "weather" and switches to tool-enabled mode&lt;/li&gt;
&lt;li&gt;Gemini calls &lt;code&gt;geocode_city("Berlin")&lt;/code&gt; to get coordinates&lt;/li&gt;
&lt;li&gt;The agent calls &lt;code&gt;get_current_weather(lat=52.52, lon=13.41)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Gemini turns the raw data into a readable response&lt;/li&gt;
&lt;li&gt;The UI renders the answer&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  6. Why This Architecture Works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;MCP separates the model from the tools.&lt;/strong&gt; The agent knows what tools exist and how to call them, but not how those tools are implemented. That makes the system easier to evolve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same server can serve different models.&lt;/strong&gt; Gemini is just the model in this demo. The MCP server itself can work with any compatible client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lazy initialization keeps the app resilient.&lt;/strong&gt; The agent can boot even if the MCP server is temporarily unavailable, and tool support only activates when it is actually needed.&lt;/p&gt;




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

&lt;p&gt;This sample is a solid starting point. Natural next steps include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose&lt;/strong&gt; - run all services together&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL persistence&lt;/strong&gt; - durable chat history and richer memory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth2&lt;/strong&gt; - authenticated multi-user access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket streaming&lt;/strong&gt; - token-by-token responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes&lt;/strong&gt; - scale the agent and tool server independently&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ykpraveen/mcp-spring-sample" rel="noopener noreferrer"&gt;Source code on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.spring.io/spring-ai/reference/api/mcp.html" rel="noopener noreferrer"&gt;Spring AI MCP documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ai.google.dev/gemini-api/docs" rel="noopener noreferrer"&gt;Google Gemini API documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Have you built anything with MCP and Spring AI? I'd love to hear how you approached it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>spring</category>
      <category>mcp</category>
      <category>react</category>
    </item>
    <item>
      <title>Building an Order Processing Pipeline with Spring Integration (HTTP + File Polling)</title>
      <dc:creator>Praveen Yadav</dc:creator>
      <pubDate>Wed, 24 Jun 2026 07:57:53 +0000</pubDate>
      <link>https://dev.to/ykpraveen/building-an-order-processing-pipeline-with-spring-integration-http-file-polling-i6a</link>
      <guid>https://dev.to/ykpraveen/building-an-order-processing-pipeline-with-spring-integration-http-file-polling-i6a</guid>
      <description>&lt;p&gt;If you’ve used Spring Boot REST APIs but haven’t explored &lt;strong&gt;Spring Integration&lt;/strong&gt; yet, this project is a practical way to see what message-driven flow design looks like in real code.&lt;/p&gt;

&lt;p&gt;I built a sample app that processes orders from two different entry points:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;HTTP API&lt;/strong&gt; (&lt;code&gt;POST /api/orders&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File polling&lt;/strong&gt; (drop CSV files into &lt;code&gt;input/&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both inputs share the same core processing logic.&lt;/p&gt;

&lt;p&gt;👉 Source code: &lt;strong&gt;&lt;a href="https://github.com/ykpraveen/spring-integration-sample" rel="noopener noreferrer"&gt;https://github.com/ykpraveen/spring-integration-sample&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What this project does
&lt;/h2&gt;

&lt;p&gt;Each order goes through this pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Transform input payload into &lt;code&gt;Order&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Validate (&lt;code&gt;id&lt;/code&gt;, &lt;code&gt;customer&lt;/code&gt;, &lt;code&gt;total&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Route by amount:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;total &amp;lt;= 100&lt;/code&gt; → EXPRESS&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;total &amp;gt; 100&lt;/code&gt; → REVIEW&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Persist order (JPA)&lt;/li&gt;
&lt;li&gt;Fan out to:

&lt;ul&gt;
&lt;li&gt;archive output file&lt;/li&gt;
&lt;li&gt;summary aggregation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Runtime persistence uses &lt;strong&gt;PostgreSQL&lt;/strong&gt;, and tests use &lt;strong&gt;H2&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Java 21&lt;/li&gt;
&lt;li&gt;Spring Boot 4.1&lt;/li&gt;
&lt;li&gt;Spring Integration 7 (Java DSL)&lt;/li&gt;
&lt;li&gt;Spring Data JPA&lt;/li&gt;
&lt;li&gt;PostgreSQL (runtime)&lt;/li&gt;
&lt;li&gt;H2 (test)&lt;/li&gt;
&lt;li&gt;Maven&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Architecture at a glance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) HTTP flow
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;POST /api/orders&lt;/code&gt; sends raw JSON to a messaging gateway, then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;JsonToOrderTransformer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OrderValidationService&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;content-based router (&lt;code&gt;EXPRESS&lt;/code&gt; / &lt;code&gt;REVIEW&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OrderStore.put(...)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;publish-subscribe to archive + summary + HTTP response message&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) File flow
&lt;/h3&gt;

&lt;p&gt;The poller watches &lt;code&gt;input/*.csv&lt;/code&gt;, then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FileToStringTransformer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CsvToOrderTransformer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;validation + routing + persistence&lt;/li&gt;
&lt;li&gt;publish-subscribe to archive + summary&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3) Error handling
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;HTTP errors return JSON via dedicated HTTP error handling.&lt;/li&gt;
&lt;li&gt;File processing errors generate failure logs in &lt;code&gt;output/failed/&lt;/code&gt; and move bad source files to &lt;code&gt;input/failed/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Spring Integration here?
&lt;/h2&gt;

&lt;p&gt;Using Spring Integration made these parts clean and explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Routing rules&lt;/strong&gt; are declarative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan-out behavior&lt;/strong&gt; is easy with publish-subscribe channels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry advice&lt;/strong&gt; can be attached per handler in the file pipeline.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;integration graph&lt;/strong&gt; (&lt;code&gt;/api/integration/graph&lt;/code&gt;) helps visualize the runtime flow.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Example requests
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Submit an EXPRESS order
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/api/orders &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"id":"ORD-001","customer":"Alice","description":"Book","total":25.00}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Submit a REVIEW order
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/api/orders &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"id":"ORD-002","customer":"Bob","description":"Laptop","total":1500.00}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Get one order
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8080/api/orders/ORD-001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  List all orders
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8080/api/orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Running locally
&lt;/h2&gt;

&lt;p&gt;Clone the repo first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ykpraveen/spring-integration-sample.git
&lt;span class="nb"&gt;cd &lt;/span&gt;spring-integration-sample
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run:&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;
mvn clean package
mvn spring-boot:run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then test HTTP endpoints or drop a CSV file into &lt;code&gt;input/&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;The project currently has &lt;strong&gt;38 tests&lt;/strong&gt; (unit + integration), covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transformation and validation&lt;/li&gt;
&lt;li&gt;HTTP happy paths and failure paths&lt;/li&gt;
&lt;li&gt;duplicate order handling (&lt;code&gt;409 Conflict&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;file poller processing and retries&lt;/li&gt;
&lt;li&gt;summary aggregation behavior via Spring Integration aggregator + writer service&lt;/li&gt;
&lt;li&gt;integration graph endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can run these directly from the repo: &lt;strong&gt;&lt;a href="https://github.com/ykpraveen/spring-integration-sample" rel="noopener noreferrer"&gt;https://github.com/ykpraveen/spring-integration-sample&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Key implementation details I found useful
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use correlation IDs&lt;/strong&gt; in headers and push them into MDC for traceable logs across flow steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep config split by concern&lt;/strong&gt; (&lt;code&gt;HttpIntegrationConfig&lt;/code&gt;, &lt;code&gt;FileIntegrationConfig&lt;/code&gt;, shared config) instead of one huge integration config class.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat summary aggregation as stateful logic&lt;/strong&gt;: clear release rules (batch size vs timeout), stable keys, and append-safe file writing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer explicit web path-variable lookup&lt;/strong&gt; for &lt;code&gt;GET /api/orders/{id}&lt;/code&gt; over indirect URL parsing.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;

&lt;p&gt;You can clone the project and run it as a demo starter for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring Integration basics&lt;/li&gt;
&lt;li&gt;event/message-driven service design in Spring&lt;/li&gt;
&lt;li&gt;hybrid ingestion patterns (HTTP + file)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re learning Spring Integration, this pattern is a good stepping stone before Kafka/Rabbit-based distributed flows.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;strong&gt;&lt;a href="https://github.com/ykpraveen/spring-integration-sample" rel="noopener noreferrer"&gt;https://github.com/ykpraveen/spring-integration-sample&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>springintegration</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
