<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="https://feeds.feedblitz.com/feedblitz_rss.xslt"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:webfeeds="http://webfeeds.org/rss/1.0"  xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0">
<channel>
	<title>Baeldung</title>
	<atom:link href="https://www.baeldung.com/feed" rel="self" type="application/rss+xml" />
	<link>https://www.baeldung.com</link>
	<description>Java, Spring and Web Development tutorials</description>
	<lastBuildDate>Fri, 07 Aug 2026 03:22:20 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
<meta xmlns="http://www.w3.org/1999/xhtml" name="robots" content="noindex" />
<item>
<feedburner:origLink>https://www.baeldung.com/spring-ai-google-genai-chat</feedburner:origLink>
		<title>Introduction to Google GenAI Chat and Spring AI</title>
		<link>https://feeds.feedblitz.com/~/967131092/0/baeldung</link>
					<comments>https://feeds.feedblitz.com/~/967131092/0/baeldung#respond</comments>
		
		<dc:creator><![CDATA[Sudarshan Hiray]]></dc:creator>
		<pubDate>Fri, 07 Aug 2026 03:22:20 +0000</pubDate>
				<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Spring AI ChatClient]]></category>
		<guid isPermaLink="false">https://www.baeldung.com/?p=204581</guid>
					<description><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="max-width:100% !important;height:auto !important;float: left; margin-right: 5px;" fetchpriority="high" /><p>Learn how to integrate Google’s Gemini models into a Spring Boot application using the Gemini Developer API.</p>
<p>The post <a rel="NOFOLLOW" href="https://feeds.feedblitz.com/~/967131092/0/baeldung">Introduction to Google GenAI Chat and Spring AI</a> first appeared on <a rel="NOFOLLOW" href="https://www.baeldung.com">Baeldung</a>.</p><div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/967131092/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/967131092/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f07%2fJava-Featured-12-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/967131092/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/967131092/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/967131092/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/spring-ai-google-genai-chat#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/spring-ai-google-genai-chat/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</description>
										<content:encoded><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="float: left; margin-right: 5px;" decoding="async" srcset="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-1024x536.jpg 1024w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-300x157.jpg 300w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-768x402.jpg 768w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-100x52.jpg 100w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12.jpg 1200w" sizes="(max-width: 580px) 100vw, 580px" /><h2 data-path-to-node="0" id="bd-introduction" data-id="introduction">1. Introduction</h2>
<div class="bd-anchor" id="introduction"></div>
<p data-path-to-node="1">The generative AI landscape is evolving rapidly, pushing framework maintainers to rethink how applications connect to diverse large language models (LLMs). With the release of <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-ai">Spring AI</a>, Java developers gained a unified, portable interface to interact with cognitive services.</p>
<p data-path-to-node="2">However, as the ecosystem matured, integrating multiple foundational models within a single runtime classpath introduced configuration conflicts. To solve this, <strong>Spring AI introduced major architectural updates. These include the specialized <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-starter-model-google-genai"><em>spring-ai-starter-model-google-genai</em> starter</a> module and an explicit model selection paradigm.</strong></p>
<p data-path-to-node="3">In this tutorial, we&#8217;ll explore how to integrate Google’s Gemini models into a Spring Boot application using the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://docs.spring.io/spring-ai/reference/api/chat/google-genai-chat.html#_option_1_gemini_developer_api_api_key">Gemini Developer API</a> (via Google AI Studio). We&#8217;ll walk through a streamlined project setup, dive into text generation patterns using both low-level and high-level APIs, apply prompt templates, enable live web grounding, and stream tokens reactively with WebFlux.</p>
<h2 data-path-to-node="4" id="bd-project-setup" data-id="project-setup">2. Project Setup</h2>
<div class="bd-anchor" id="project-setup"></div>
<p data-path-to-node="5">To get started, we need a standard Spring Boot 3.x application. Because Spring AI modules are actively updated, it&#8217;s highly recommended to manage dependency versions using the Spring AI <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-maven-bom">Bill of Materials (BOM)</a>.</p>
<h3 data-path-to-node="6" id="bd-1-maven-dependencies" data-id="1-maven-dependencies">2.1. Maven Dependencies</h3>
<div class="bd-anchor" id="1-maven-dependencies"></div>
<p data-path-to-node="7">First, let&#8217;s configure the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-starter-model-google-genai">Spring AI Starter Google GenAI</a> and <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-bom/2.0.0">Spring AI BOM</a> in our <em>pom.xml </em>file. <strong>Since Spring AI artifacts are hosted in the Spring Milestones repository during release cycles, we ensure both the repository and dependency management blocks are properly declared</strong>:</p>
<pre><code class="language-java">&lt;project&gt;
    &lt;dependencyManagement&gt;
        &lt;dependencies&gt;
            &lt;dependency&gt;
                &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
                &lt;artifactId&gt;spring-ai-bom&lt;/artifactId&gt;
                &lt;version&gt;2.0.0&lt;/version&gt;
                &lt;type&gt;pom&lt;/type&gt;
                &lt;scope&gt;import&lt;/scope&gt;
            &lt;/dependency&gt;
        &lt;/dependencies&gt;
    &lt;/dependencyManagement&gt;
    &lt;dependencies&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
            &lt;artifactId&gt;spring-ai-starter-model-google-genai&lt;/artifactId&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;
&lt;/project&gt;</code></pre>
<h3 data-path-to-node="5" id="bd-2-activating-the-chat-provider-via-springaimodelchat" data-id="2-activating-the-chat-provider-via-springaimodelchat">2.2. Activating the Chat Provider via <em>spring.ai.model.chat</em></h3>
<div class="bd-anchor" id="2-activating-the-chat-provider-via-springaimodelchat"></div>
<p data-path-to-node="6">In the latest versions of Spring AI, dropping a starter module onto the classpath no longer automatically activates it. This prevents runtime configuration conflicts when your application references multiple LLM providers.</p>
<p data-path-to-node="7"><strong>We must explicitly declare our active chat provider inside our <em>application.properties</em> file</strong>:</p>
<pre><code class="language-java">spring.ai.model.chat=google-genai
spring.ai.google.genai.chat.model=gemini-2.5-flash
spring.ai.google.genai.api-key=${SPRING_AI_GOOGLE_GENAI_API_KEY}</code></pre>
<p>Obtain an API key from the <a class="external" href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://aistudio.google.com/app/apikey" target="_blank" rel="noopener">Google AI Studio</a>. Using this placeholder syntax explicitly documents the application&#8217;s external runtime dependency while preventing hardcoded secrets from leaking into our source control repository.</p>
<h3 data-path-to-node="10" id="bd-3-environment-setup" data-id="3-environment-setup">2.3. Environment Setup</h3>
<div class="bd-anchor" id="3-environment-setup"></div>
<p data-path-to-node="11">We can satisfy the property placeholder defined above without writing any custom Java decryption or binding code. <strong>We&#8217;ll simply export the credentials directly into our environment</strong>. The auto-configuration engine will automatically resolve the placeholder value at startup:</p>
<pre><code class="language-java">export SPRING_AI_GOOGLE_GENAI_API_KEY="YourActualSecureGoogleAIStudioKeyHere"</code></pre>
<p>Once this environment variable is set, Spring Boot seamlessly bridges the variable to the properties file placeholder, making the<em> GoogleGenAiChatModel</em> ready for injection.</p>
<h2 data-path-to-node="22" id="bd-core-text-and-content-generation-strategies" data-id="core-text-and-content-generation-strategies">3. Core Text and Content Generation Strategies</h2>
<div class="bd-anchor" id="core-text-and-content-generation-strategies"></div>
<p data-path-to-node="23">Spring AI gives us two primary abstraction layers for communicating with Gemini models: <strong>the foundational <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-artificial-intelligence-structure-output#brief-introduction-to-the-chat-model"><em>ChatModel</em></a> bean and the highly configurable, fluent <em>ChatClient</em> API. </strong> We isolate our interaction logic into a dedicated service layer to follow enterprise design patterns and keep code clean. Then, we expose these capabilities through standard Spring REST controllers.</p>
<h3 data-path-to-node="24" id="bd-1-injecting-and-using-googlegenaichatmodel" data-id="1-injecting-and-using-googlegenaichatmodel">3.1. Injecting and Using <em>GoogleGenAiChatModel</em></h3>
<div class="bd-anchor" id="1-injecting-and-using-googlegenaichatmodel"></div>
<p data-path-to-node="24">The <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/google/genai/GoogleGenAiChatModel.html"><em>GoogleGenAiChatModel </em></a>represents the foundational, low-level client abstraction.<strong> It handles request serialization, HTTP execution against Google&#8217;s RPC endpoints, and raw response parsing.</strong></p>
<p data-path-to-node="25">Let&#8217;s look at our <em>ChatService, </em>where we inject this bean along with a <em>ChatClient.Builder</em> to initialize our operational layers:</p>
<pre><code class="language-java">@Service
public class ChatService {
    private final GoogleGenAiChatModel chatModel;
    private final ChatClient defaultChatClient;
    private final ChatClient fluentChatClient;
    public ChatService(GoogleGenAiChatModel chatModel, ChatClient.Builder chatClientBuilder) {
        this.chatModel = chatModel;
        this.defaultChatClient = chatClientBuilder.build();
        this.fluentChatClient = chatClientBuilder
          .defaultSystem("You are a concise technical writer summarizing software concepts.")
          .build();
    }
    public String simplifiedPrompt(String message) {
        return chatModel.call(message);
    }
}</code></pre>
<p data-path-to-node="28">When we invoke <em>chatModel.call(message)</em>, the framework packages the raw string into a default prompt context. It then sends this to the configured model variant, extracts the text block from the response payload, and returns it. We expose this in our <em>ChatController</em>:</p>
<pre><code class="language-java">@RestController
public class ChatController {
    private final ChatService chatService;
    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }
    @GetMapping("/v1/chat/simple")
    public String simplifiedPrompt(@RequestParam(defaultValue = "Hello") String message) {
        return chatService.simplifiedPrompt(message);
    }
}</code></pre>
<p data-path-to-node="29">This establishes a simple string-in, string-out endpoint. It uses the lowest-level model client abstraction available to verify basic connectivity with the Gemini engine.</p>
<h3 data-path-to-node="29" id="bd-2-constructing-prompts-with-the-fluent-chatclient-api" data-id="2-constructing-prompts-with-the-fluent-chatclient-api">3.2. Constructing Prompts With the Fluent <em>ChatClient</em> API</h3>
<div class="bd-anchor" id="2-constructing-prompts-with-the-fluent-chatclient-api"></div>
<p data-path-to-node="30">While working directly with the model handles quick operations, production architectures favor the fluent <em>ChatClient </em>API. <strong>The <em>ChatClient </em>acts as a facade layer that simplifies prompt assembly and attaches default configuration baselines.</strong></p>
<p data-path-to-node="31">As shown in our service constructor, we pre-configured a specialized <span style="margin: 0px;padding: 0px">fluent<em>ChatClient </em>with</span> system instructions. Let&#8217;s add the corresponding execution method to <em>ChatService</em>:</p>
<pre><code class="language-java">public String fluentPrompt(String prompt) {
    return this.fluentChatClient.prompt()
      .user(prompt)
      .call()
      .content();
}</code></pre>
<p data-path-to-node="33">By routing requests through this pre-configured instance, every user query automatically appends the target system instructions before execution. We expose this endpoint in the controller:</p>
<pre><code class="language-java">@GetMapping("/v1/chat/fluent")
public String fluentPrompt(@RequestParam String prompt) {
    return chatService.fluentPrompt(prompt);
}</code></pre>
<p data-path-to-node="35">This allows us to encapsulate recurring personas or systemic behavior directly within the client wrapper instance. As a result, we don&#8217;t need to modify individual incoming text arguments manually.</p>
<h3 data-path-to-node="35" id="bd-3-handling-dynamic-inputs-via-prompt-templates" data-id="3-handling-dynamic-inputs-via-prompt-templates">3.3. Handling Dynamic Inputs via Prompt Templates</h3>
<div class="bd-anchor" id="3-handling-dynamic-inputs-via-prompt-templates"></div>
<p data-path-to-node="36">Hardcoding parameter logic directly inside strings leads to messy string concatenation and fragile codebases. <strong>Spring AI solves this with structured prompt templates, separating instructions from user variables.</strong></p>
<p data-path-to-node="37">Let&#8217;s implement a code review capability inside our <em>ChatService </em>using a multiline text block placeholder layout:</p>
<pre><code class="language-java">public String reviewCode(String language, String codeSnippet) {
    String template = """
            Analyze the following {language} code snippet for memory leaks or inefficiencies.
            Provide an optimized version.
            Code:
            {code}
            """;
    return this.defaultChatClient.prompt()
      .user(u -&gt; u.text(template).params(Map.of("language", language, "code", codeSnippet)))
      .call()
      .content();
}</code></pre>
<p>The client replaces the target tags (<em>{language}</em>,<em> {code}</em>) at runtime, formatting the text cleanly before submission. We expose this via a <em>POST </em>request wrapper in our controller:</p>
<pre><code class="language-java">@PostMapping("/v1/chat/review")
public String reviewCode(@RequestParam(defaultValue = "Java") String language, @RequestBody String codeSnippet) {
    return chatService.reviewCode(language, codeSnippet);
}</code></pre>
<p data-path-to-node="39">This pattern cleanly decouples structural prompt engineering boundaries from volatile business data, producing a highly reusable, parameter-driven invocation pattern.</p>
<h3 data-path-to-node="39" id="bd-4-grounding-responses-with-live-google-search-retrieval" data-id="4-grounding-responses-with-live-google-search-retrieval">3.4. Grounding Responses With Live Google Search Retrieval</h3>
<div class="bd-anchor" id="4-grounding-responses-with-live-google-search-retrieval"></div>
<p data-path-to-node="40">LLMs naturally suffer from training cut-off windows and knowledge gaps regarding live, real-world developments.<strong> The Google GenAI module exposes an explicit grounding property to seamlessly connect our model with live Google Search indexing.</strong></p>
<p data-path-to-node="41">To activate live search grounding across our entire application, we need to toggle the grounding flag to <em>true</em> inside our configuration properties:</p>
<pre><code class="language-java">spring.ai.google.genai.chat.google-search-retrieval=true</code></pre>
<p>When this property is enabled, queries regarding current events or breaking news are automatically grounded using fresh search results. Let&#8217;s write an endpoint to handle real-time informational requests:</p>
<pre><code class="language-java">public String searchGroundedPrompt(String currentEventQuery) {
    return this.defaultChatClient.prompt()
      .user(currentEventQuery)
      .call()
      .content();
}</code></pre>
<p data-path-to-node="45">We map this method to our HTTP layer inside our controller configuration:</p>
<pre><code class="language-java">@GetMapping("/v1/chat/grounded")
public String searchGroundedPrompt(@RequestParam String currentEventQuery) {
    return chatService.searchGroundedPrompt(currentEventQuery);
}</code></pre>
<p data-path-to-node="45">If we pass a request like <i data-path-to-node="33" data-index-in-node="26">&#8220;Who won the most recent football tournament match yesterday?&#8221;</i>, the model uses live Google Search indexing to anchor its response in verified facts, drastically reducing hallucinations. This configuration bridges the gap between static training cutoffs and live, real-world events.</p>
<h3 data-path-to-node="49" id="bd-5-processing-real-time-streaming-responses-with-flux" data-id="5-processing-real-time-streaming-responses-with-flux">3.5. Processing Real-Time Streaming Responses With <em>Flux</em></h3>
<div class="bd-anchor" id="5-processing-real-time-streaming-responses-with-flux"></div>
<p data-path-to-node="50">For user-facing interfaces, waiting for a long text block to fully generate on the server results in noticeable latency.<strong> Instead, we can stream chunks back to the user, token by token, using Spring Boot&#8217;s WebFlux integration.</strong></p>
<p data-path-to-node="51">Let&#8217;s add a streaming method to our <span style="margin: 0px;padding: 0px"><em>ChatService </em>that</span> returns a reactive <em>Flux&lt;String&gt;</em> structure:</p>
<pre><code class="language-java">public Flux&lt;String&gt; streamChatTokens(String prompt) {
    return this.defaultChatClient.prompt()
      .user(prompt)
      .stream()
      .content();
}</code></pre>
<p data-path-to-node="50">We&#8217;ll create a specialized <em>StreamingChatController </em>to deliver these tokens cleanly over an open connection pipeline. This controller explicitly produces a <em>text/event-stream</em> media response:</p>
<pre><code class="language-java">@RestController
public class StreamingChatController {
    private final ChatService chatService;
    public StreamingChatController(ChatService chatService) {
        this.chatService = chatService;
    }
    @GetMapping(value = "/v1/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux&lt;String&gt; streamChatTokens(@RequestParam String prompt) {
        return chatService.streamChatTokens(prompt);
    }
}</code></pre>
<p data-path-to-node="50">When hitting this endpoint, consumers read token data reactively.  This establishes a non-blocking execution pipeline that builds an interactive, low-latency UI flow chunk by chunk.</p>
<h2 data-path-to-node="55" id="bd-testing" data-id="testing">4. Testing</h2>
<div class="bd-anchor" id="testing"></div>
<p data-path-to-node="56">Testing applications that rely on external generative AI endpoints requires a clear separation of concerns. <strong>Running tests shouldn&#8217;t trigger external network calls, hit rate limits, or exhaust API quotas during local compilation</strong>. To prevent this, we isolate our HTTP layer using Spring Boot&#8217;s slice testing along with <em>@MockitoBean</em>.</p>
<h3 data-path-to-node="57" id="bd-1-unit-testing-via-weblayer-mockmvc-mocking" data-id="1-unit-testing-via-weblayer-mockmvc-mocking">4.1. Unit Testing via WebLayer MockMvc Mocking</h3>
<div class="bd-anchor" id="1-unit-testing-via-weblayer-mockmvc-mocking"></div>
<p data-path-to-node="58">Since our implementation encapsulates all LLM communications within <em>ChatService</em>, we can completely mock this business component. <strong>This permits us to cleanly assert controller routing, request parameters, response body expectations, and stream-handling patterns.</strong></p>
<p data-path-to-node="59">Let&#8217;s write an isolated test using <em>MockMvc </em>to verify all text-based, template-driven, and reactive stream endpoints without triggering an actual network handshake. First, we set up our simple prompt test to verify basic GET parameter routing and raw response mapping:</p>
<pre><code class="language-java">@SpringBootTest(properties = {
    "spring.ai.model.chat=google-genai",
    "spring.ai.google.genai.chat.model=gemini-2.5-flash",
    "spring.ai.google.genai.api-key=test-key"
})
@AutoConfigureMockMvc
class ChatControllerUnitTest {
    @Autowired
    private MockMvc mockMvc;
    @MockBean
    private ChatService chatService;
    @Test
    void whenSimpleEndpointIsInvoked_thenReturnsMockedModelResponse() throws Exception {
        when(chatService.simplifiedPrompt(anyString())).thenReturn("Mocked Gemini Response");
        mockMvc.perform(get("/v1/chat/simple").param("message", "Hello"))
          .andExpect(status().isOk())
          .andExpect(content().string("Mocked Gemini Response"));
        verify(chatService).simplifiedPrompt("Hello");
    }
}
</code></pre>
<p>Next, we confirm that our fluent prompt endpoint forwards query parameters accurately to the underlying service. Then it returns the configured <em>system-prompt-backed</em> response:</p>
<pre><code class="language-java">@Test
void whenFluentEndpointIsInvoked_thenReturnsModelContent() throws Exception {
    when(chatService.fluentPrompt(anyString())).thenReturn("Fluent Response");
    mockMvc.perform(get("/v1/chat/fluent").param("prompt", "Explain DI"))
      .andExpect(status().isOk())
      .andExpect(content().string("Fluent Response"));
    verify(chatService).fluentPrompt("Explain DI");
}</code></pre>
<p>For our template-driven endpoint, we validate that the controller correctly processes a multipart payload consisting of query parameters and a <em>text/plain</em> request body:</p>
<pre><code class="language-java">@Test
void whenReviewEndpointIsInvoked_thenReturnsModelContent() throws Exception {
    when(chatService.reviewCode(anyString(), anyString())).thenReturn("Optimized Code");
    mockMvc.perform(post("/v1/chat/review")
        .param("language", "Java")
        .contentType(MediaType.TEXT_PLAIN)
        .content("class A { }"))
      .andExpect(status().isOk())
      .andExpect(content().string("Optimized Code"));
    verify(chatService).reviewCode("Java", "class A { }");
}</code></pre>
<pre></pre>
<p>We&#8217;ll now verify that search-grounded queries pass their parameters cleanly through the controller layer without parameter binding errors:</p>
<pre><code class="language-java">@Test
void whenGroundedEndpointIsInvoked_thenReturnsModelContent() throws Exception {
    when(chatService.searchGroundedPrompt(anyString())).thenReturn("Grounded Response");
    mockMvc.perform(get("/v1/chat/grounded").param("currentEventQuery", "latest match winner"))
      .andExpect(status().isOk())
      .andExpect(content().string("Grounded Response"));
    verify(chatService).searchGroundedPrompt("latest match winner");
}</code></pre>
<p>Finally, we test our reactive endpoint to ensure Spring MVC properly negotiates the <em>text/event-stream</em> media type. It will also format the reactive <em>Flux </em>elements into standard Server-Sent Event (<em>data:</em>) frames:</p>
<pre><code class="language-java">@Test
void whenStreamEndpointIsInvoked_thenReturnsEventStreamContent() throws Exception {
    when(chatService.streamChatTokens(anyString()))
      .thenReturn(reactor.core.publisher.Flux.just("token-1", "token-2"));
    mockMvc.perform(get("/v1/chat/stream").param("prompt", "Stream tokens"))
      .andExpect(status().isOk())
      .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_EVENT_STREAM))
      .andExpect(content().string("data:token-1\n\ndata:token-2\n\n"));
    
    verify(chatService).streamChatTokens("Stream tokens");
}</code></pre>
<p>By leveraging the modern <em>@MockBean </em>annotation, we inject our mock definitions directly into the application context wrapper, replacing the real bean configuration. <strong>This allows us to stub out standard synchronous operations as well as complex reactive <em>Flux </em>stream responses safely and reliably without network overhead.</strong></p>
<h2 data-path-to-node="57" id="bd-conclusion" data-id="conclusion">5. Conclusion</h2>
<div class="bd-anchor" id="conclusion"></div>
<p data-path-to-node="64">In this tutorial, we configured a Spring Boot application using Spring AI&#8217;s updated <em>spring-ai-starter-model-google-genai</em> engine to interact directly with Google AI Studio.</p>
<p data-path-to-node="64">We saw how explicitly defining <em>spring.ai.model.chat=google-genai</em> resolves modern multi-model classpath dependencies. From there, we established a clean architecture using a dedicated <em>ChatService</em>. We built out flexible prompt workflows with the fluent <em>ChatClient </em>API and isolated dynamic parameters using templates. Finally, we enabled live web grounding and streamed responsive outputs via WebFlux <em>Flux.</em></p>
<p data-path-to-node="43">As always, the complete code samples used in this article are available <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/eugenp/tutorials/tree/master/spring-ai-modules">over on GitHub</a>.</p><p>The post <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-ai-google-genai-chat">Introduction to Google GenAI Chat and Spring AI</a> first appeared on <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com">Baeldung</a>.</p><Img align="left" border="0" height="1" width="1" alt="" style="border:0;float:left;margin:0;padding:0;width:1px!important;height:1px!important;" hspace="0" src="https://feeds.feedblitz.com/~/i/967131092/0/baeldung">
<div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/967131092/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/967131092/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f07%2fJava-Featured-12-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/967131092/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/967131092/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/967131092/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/spring-ai-google-genai-chat#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/spring-ai-google-genai-chat/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</content:encoded>
					
					<wfw:commentRss>https://feeds.feedblitz.com/~/967131092/0/baeldung/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<webfeeds:featuredImage>https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-150x150.jpg</webfeeds:featuredImage></item>
<item>
<feedburner:origLink>https://www.baeldung.com/spring-ai-automemorytools</feedburner:origLink>
		<title>Spring AI AutoMemoryTools</title>
		<link>https://feeds.feedblitz.com/~/967131095/0/baeldung</link>
					<comments>https://feeds.feedblitz.com/~/967131095/0/baeldung#respond</comments>
		
		<dc:creator><![CDATA[Stelios Anastasakis]]></dc:creator>
		<pubDate>Fri, 07 Aug 2026 03:11:28 +0000</pubDate>
				<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Spring AI ChatClient]]></category>
		<guid isPermaLink="false">https://www.baeldung.com/?p=204568</guid>
					<description><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="max-width:100% !important;height:auto !important;float: left; margin-right: 5px;" /><p>Learn how an agent decides what to remember about a user or project with Spring AI AutoMemoryTools.</p>
<p>The post <a rel="NOFOLLOW" href="https://feeds.feedblitz.com/~/967131095/0/baeldung">Spring AI AutoMemoryTools</a> first appeared on <a rel="NOFOLLOW" href="https://www.baeldung.com">Baeldung</a>.</p><div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/967131095/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/967131095/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f07%2fJava-Featured-12-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/967131095/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/967131095/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/967131095/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/spring-ai-automemorytools#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/spring-ai-automemorytools/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</description>
										<content:encoded><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="float: left; margin-right: 5px;" decoding="async" loading="lazy" srcset="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-1024x536.jpg 1024w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-300x157.jpg 300w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-768x402.jpg 768w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-100x52.jpg 100w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12.jpg 1200w" sizes="auto, (max-width: 580px) 100vw, 580px" /><h2 id="bd-overview" data-id="overview">1. Overview</h2>
<div class="bd-anchor" id="overview"></div>
<p>As AI agents become more capable, long-term memory has become just as important as reasoning. While mechanisms such as <em><a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-ai-chat-memory">ChatMemory</a></em> and <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-ai-redis-rag-app">Retrieval Augmented Generation</a> (RAG) handle conversations and external knowledge, they don&#8217;t answer a different question: What should an agent permanently remember about a user or project?</p>
<p>Spring AI AutoMemoryTools addresses this by enabling agents to persist and recall long-term memories, such as user preferences and project-specific knowledge.</p>
<p>In this article, we&#8217;ll explore AutoMemoryTools, its architecture and components, and build a simple Spring Boot chatbot that demonstrates how it works.</p>
<h2 id="bd-dependencies" data-id="dependencies">2. Dependencies</h2>
<div class="bd-anchor" id="dependencies"></div>
<p><strong>Let&#8217;s start by defining the minimum dependencies possible for demonstrating the Spring AI AutoMemoryTools</strong>. We&#8217;ll need the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-starter-model-openai">spring-ai-starter-model-openai</a> and <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.springaicommunity/spring-ai-agent-utils">spring-ai-agent-utils</a>. The first is the starter for integrating any OpenAI-compatible API with Spring AI, and the latter contains the tools we&#8217;re exploring in this article. We can use the (current) latest versions:</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
    &lt;artifactId&gt;spring-ai-starter-model-openai&lt;/artifactId&gt;
    &lt;version&gt;2.0.0&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springaicommunity&lt;/groupId&gt;
    &lt;artifactId&gt;spring-ai-agent-utils&lt;/artifactId&gt;
    &lt;version&gt;0.10.0&lt;/version&gt;
&lt;/dependency&gt;</code></pre>
<h2 id="bd-spring-ai-automemorytools" data-id="spring-ai-automemorytools">3. Spring AI AutoMemoryTools</h2>
<div class="bd-anchor" id="spring-ai-automemorytools"></div>
<p><strong>Spring AI AutoMemoryTools is a Spring AI Community library that implements long-term memory for AI agents. It uses tool calling and file-based persistence</strong>. It&#8217;s part of the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/spring-ai-community/spring-ai-agent-utils">Spring AI Agent Utils</a>, similar to <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-ai-agent-skills">Agent Skills</a>. It&#8217;s worth mentioning that it&#8217;s inspired heavily by Claude Code and saves developers from implementing common agent capabilities themselves.</p>
<p>Similar to <em>MessageChatMemoryAdvisor</em> (for conversation memory), Spring AI AutoMemoryTools also implements <em>BaseChatMemoryAdvisor</em>. This means it&#8217;s related to conversational memory, but it does not focus on the conversation part. Rather, aims to store information about concepts like user preferences, projects, feedback, and more.</p>
<h3 id="bd-1-types-of-memory" data-id="1-types-of-memory">3.1. Types of Memory</h3>
<div class="bd-anchor" id="1-types-of-memory"></div>
<p><strong>Let&#8217;s look at the types of memory the Spring AI AutoMemoryTools targets</strong>:</p>
<ul>
<li>User: Remembers who the user is and how to assist them best</li>
<li>Feedback: Remembers how the user prefers the AI to behave</li>
<li>Project: Remembers important project context and decisions</li>
<li>Reference: Remembers where to find information in external systems.</li>
</ul>
<p>As we&#8217;ll see later, these are best described in the <em>AUTO_MEMORY_TOOLS_SYSTEM_PROMPT.md</em>, which is part of the dependency resources. Another thing we&#8217;ll see is how we can override this <em>PromptSystemSpec</em> with a custom one. This would mean that these types of memory might no longer exist, unless we define them.</p>
<h3 id="bd-2-the-tools" data-id="2-the-tools">3.2. The Tools</h3>
<div class="bd-anchor" id="2-the-tools"></div>
<p><strong>Now, let&#8217;s go through the six distinct tools of AutoMemoryTools</strong>:</p>
<ul>
<li>MemoryView: Reads the contents of a memory file or lists the available memory files in a directory</li>
<li>MemoryCreate: Creates a new memory file to persist long-term information</li>
<li>MemoryStrReplace: Updates an existing memory by replacing an exact piece of text</li>
<li>MemoryInsert: Inserts new content at a specific location within an existing memory file</li>
<li>MemoryDelete: Removes a memory file or directory from persistent storage</li>
<li>MemoryRename: Renames or relocates a memory file while preserving its contents.</li>
</ul>
<h3 id="bd-3-memory-system-prompt" data-id="3-memory-system-prompt">3.3. Memory System Prompt</h3>
<div class="bd-anchor" id="3-memory-system-prompt"></div>
<p>As we saw before, the <em>AUTO_MEMORY_TOOLS_SYSTEM_PROMPT.md</em> is part of the library. <strong>The library offers two different levels of abstraction. The first is sandboxed, a restricted environment that limits the agent&#8217;s access, using the Spring AI AutoMemoryTools. The other is the unrestricted use of <em>FileSystemTools</em> or <em>ShellTools</em></strong>.</p>
<p>In practise, <em>AutoMemoryTools</em> is a specialized abstraction built on top of generic filesystem operations. The Agent can access and manage memory files within the designated memory directories like <em>memory/user</em>, <em>memory/feedback</em>, etc. But it should not have the privileges to do anything outside the memory directory. On the contrary, <em>FileSystemTools</em> and <em>ShellTools</em> are unrestricted unless the developer properly handles this matter.</p>
<h3 id="bd-4-how-it-works---bringing-all-together" data-id="4-how-it-works---bringing-all-together">3.4. How it Works &#8211; Bringing All Together</h3>
<div class="bd-anchor" id="4-how-it-works---bringing-all-together"></div>
<p>Now that we&#8217;ve covered the components, let&#8217;s see how a request flows through Spring AI AutoMemoryTools.</p>
<p><strong>When a user sends a request, the LLM combines the Memory System Prompt with the user&#8217;s message to determine whether it should read from or write to memory</strong>. If needed, it invokes the AutoMemoryTools to manage Markdown files in the configured memory directory, as described by the default <em>AUTO_MEMORY_TOOLS_SYSTEM_PROMPT.md</em>.</p>
<p>After generating the response, the LLM may perform additional memory operations, such as invoking more tools or consolidating existing memory entries. Finally, it returns the response to the user.</p>
<h2 id="bd-spring-ai-automemorytools-application" data-id="spring-ai-automemorytools-application">4. Spring AI AutoMemoryTools Application</h2>
<div class="bd-anchor" id="spring-ai-automemorytools-application"></div>
<p>Bringing all together, let&#8217;s see this in action. We&#8217;ll see three scenarios of configuring a <em>ChatClient</em> to use the Spring AI AutoMemoryTools:</p>
<ul>
<li>All-defaults <em>AutoMemoryToolsAdvisor</em> (sandbox operation)</li>
<li>Manual setup of <em>AutoMemoryToolsAdvisor</em> (sandbox operation)</li>
<li><em>FileSystemTools</em> and <em>ShellTools</em> (not sandboxed by default)</li>
</ul>
<h3 id="bd-1-default-automemorytoolsadvisor-configuration" data-id="1-default-automemorytoolsadvisor-configuration">4.1. Default AutoMemoryToolsAdvisor Configuration</h3>
<div class="bd-anchor" id="1-default-automemorytoolsadvisor-configuration"></div>
<p>For the first scenario, we create an AutoMemoryToolsAdvisor with the default, minimum configuration needed:</p>
<pre><code class="language-java">@Configuration
class ChatClientConfiguration {
    @Value("${agent.memory.dir}")
    String memoryDirectory;
    ChatClient chatClient(ChatModel chatModel) {
        return ChatClient
          .builder(chatModel)
          .defaultAdvisors(
            AutoMemoryToolsAdvisor.builder()
              .memoriesRootDirectory(memoryDirectory)
              .build(),
            MessageChatMemoryAdvisor.builder(
              MessageWindowChatMemory.builder()
                .maxMessages(100)
                .build())
            .build(),
            ToolCallingAdvisor.builder()
              .disableInternalConversationHistory()
              .build())
          .build();
    }
}</code></pre>
<p>This setup uses the default <em>AUTO_MEMORY_TOOLS_SYSTEM_PROMPT.md</em> and defines the memory types we explained earlier. We use the <em>Advisor</em> to intercept all message exchanges. The <em>memoriesRootDirectory()</em> method sets the memory directory, which is also the boundary of access for the Agent. Last, the <em>MessageChatMemoryAdvisor</em> is the known <em>Advisor</em> that handles the memory for the conversation aspect. We set a random value of 100 messages history.</p>
<h3 id="bd-2-custom-automemorytools-configuration" data-id="2-custom-automemorytools-configuration">4.2. Custom AutoMemoryTools Configuration</h3>
<div class="bd-anchor" id="2-custom-automemorytools-configuration"></div>
<p><strong>If we still want the sandboxed AutoMemoryTools but we have more system prompts to include</strong>, we can use a more custom approach:</p>
<pre><code class="language-java">@Configuration
class ChatClientConfiguration {
    // ...
    @Value("classpath:/prompts/AUTO_MEMORY_TOOLS_SYSTEM_PROMPT.md")
    Resource memorySystemPromptAutoMemoryTools;
    // ....
    @Bean
    ChatClient chatClientWithMoreSystemPrompt(ChatModel chatModel) {
        return ChatClient
          .builder(chatModel)
          .defaultSystem(p -&gt; p
            .text(memorySystemPromptAutoMemoryTools)
            .param("MEMORIES_ROOT_DIERCTORY", memoryDirectory))
          .defaultTools(
            AutoMemoryTools.builder()
              .memoriesDir(memoryDirectory)
              .build(),
            TodoWriteTool.builder()
              .build())
          .defaultAdvisors(ToolCallingAdvisor.builder()
              .build())
          .build();
    }
}</code></pre>
<p>Here, we do a similar setup as before, but we use the Tools instead. This way we can define the custom PromptSystemSpec, with the prompt in the <em>.md</em> file we provide. However, this prompt is the essence of AutoMemoryTools, where we should define memory types, how to store, and more. So, if we need to override it, better copy the default prompt and append the extra system prompts.</p>
<p>We should note that <em>ChatMemory</em> is not set here, but the <em>TodoWriteTool</em> is used. This has nothing to do with Spring AI AutoMemoryTools. It&#8217;s to show that we can have or not have more tools and advisors, depending on the Agent&#8217;s needs.</p>
<h3 id="bd-3-filesystemtools-and-shelltools" data-id="3-filesystemtools-and-shelltools">4.3. <em>FileSystemTools</em> and <em>ShellTools</em></h3>
<div class="bd-anchor" id="3-filesystemtools-and-shelltools"></div>
<p>Last, we can have an unrestricted scenario, using <em>FileSystemTools</em> and <em>ShellTools</em>. For example,<strong> if the agent already has FileSystemTools or ShellTools for other tasks, there&#8217;s no need for AutoMemoryTools</strong>:</p>
<pre><code class="language-java">@Configuration
class ChatClientConfiguration {
    // ...
    @Value("classpath:/prompts/AUTO_MEMORY_FILESYSTEM_TOOLS_SYSTEM_PROMPT.md")
    Resource memorySystemPromptFilesystemTools;
    // ....
    @Bean
    ChatClient chatClientWithoutAutoMemoryTools(ChatModel chatModel) {
      return ChatClient
        .builder(chatModel)
        .defaultSystem(p -&gt; p
          .text(memorySystemPromptFilesystemTools)
          .param("MEMORIES_ROOT_DIERCTORY", memoryDirectory))   // tells the agent where to write
          .defaultTools(
            ShellTools.builder()
              .build(),         // Bash — mkdir, ls, etc.
            FileSystemTools.builder()
              .build())    // Read, Write, Edit — memory file operations
          .defaultAdvisors(ToolCallingAdvisor.builder()
              .build())
        .build();
    }
}</code></pre>
<p>The file <em>AUTO_MEMORY_FILESYSTEM_TOOLS_SYSTEM_PROMPT.md</em> can also be found in the library. For the demonstration, we can copy it over. Using this one, the same memory conventions apply: <em>.md</em> files, two-step save, etc. But the agent conventionally has full filesystem access and stays in the configured directory only.</p>
<p>The two tools are the ones the Agent needs to do all the file creation, removal, editing, etc, to keep the memory in the file system.</p>
<h3 id="bd-4-other-project-files" data-id="4-other-project-files">4.4. Other Project Files</h3>
<div class="bd-anchor" id="4-other-project-files"></div>
<p>For the demonstration, we&#8217;ll use spring-web-mvc, with a <em>RestController</em>:</p>
<pre><code class="language-java">@PostMapping("/chat-with-memory")
ResponseEntity&lt;String&gt; chat(@RequestBody String question, @RequestHeader("X-Conversation-ID") String conversationId) {
    String answer = chatClient
      .prompt()
      .advisors(a -&gt; a.param(ChatMemory.CONVERSATION_ID, conversationId))
      .user(question)
      .call()
      .content();
    return ResponseEntity.ok(answer);
}</code></pre>
<p>As mentioned earlier, the <em>conversationId</em> is needed, since Spring AI AutoMemoryTools implements <em>BaseChatMemoryAdvisor</em>.</p>
<p>Let&#8217;s run the application using a local Ollama Qwen model:</p>
<p>&nbsp;</p>
<a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/wp-content/uploads/2026/08/demo-automemorytools-chat-1024x440-1.png"><img loading="lazy" decoding="async" class="alignnone wp-image-204570" src="https://www.baeldung.com/wp-content/uploads/2026/08/demo-automemorytools-chat-1024x440-1-300x129.png" alt="" width="1021" height="439" srcset="https://www.baeldung.com/wp-content/uploads/2026/08/demo-automemorytools-chat-1024x440-1-300x129.png 300w, https://www.baeldung.com/wp-content/uploads/2026/08/demo-automemorytools-chat-1024x440-1-768x330.png 768w, https://www.baeldung.com/wp-content/uploads/2026/08/demo-automemorytools-chat-1024x440-1-100x43.png 100w, https://www.baeldung.com/wp-content/uploads/2026/08/demo-automemorytools-chat-1024x440-1-600x258.png 600w, https://www.baeldung.com/wp-content/uploads/2026/08/demo-automemorytools-chat-1024x440-1.png 1024w" sizes="auto, (max-width: 1021px) 100vw, 1021px" /></a>
<p>First, we ask the Agent to save some info. Then we request this info, but notice the different <em>conversationId</em>! The Agent doesn&#8217;t know anything about <em>session-1</em>. Asking as <em>session-2</em> again, the Agent remembers it.</p>
<p>Depending on our machine, the Agent interaction with the file system might be quite slow. We can also see the directories that the Agent created, along with the file content:</p>
<p>&nbsp;</p>
<a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/wp-content/uploads/2026/08/automemorytools-folders-created-1024x332-1.png"><img loading="lazy" decoding="async" class="alignnone wp-image-204569" src="https://www.baeldung.com/wp-content/uploads/2026/08/automemorytools-folders-created-1024x332-1-300x97.png" alt="" width="1021" height="330" srcset="https://www.baeldung.com/wp-content/uploads/2026/08/automemorytools-folders-created-1024x332-1-300x97.png 300w, https://www.baeldung.com/wp-content/uploads/2026/08/automemorytools-folders-created-1024x332-1-768x249.png 768w, https://www.baeldung.com/wp-content/uploads/2026/08/automemorytools-folders-created-1024x332-1-100x32.png 100w, https://www.baeldung.com/wp-content/uploads/2026/08/automemorytools-folders-created-1024x332-1-600x195.png 600w, https://www.baeldung.com/wp-content/uploads/2026/08/automemorytools-folders-created-1024x332-1.png 1024w" sizes="auto, (max-width: 1021px) 100vw, 1021px" /></a>
<h2 id="bd-conclusion" data-id="conclusion">5. Conclusion</h2>
<div class="bd-anchor" id="conclusion"></div>
<p>In this article, we went through Spring AI AutoMemoryTools. We covered the use cases and the basic definitions. Then we walked through the six operations and the three options to use them. Finally, we used Spring AI to demonstrate it in practise.</p>
<p>As always, the source code of the examples can be found <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/eugenp/tutorials/tree/master/spring-ai-modules/spring-ai-agent-skills">over on GitHub</a>.</p><p>The post <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-ai-automemorytools">Spring AI AutoMemoryTools</a> first appeared on <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com">Baeldung</a>.</p><Img align="left" border="0" height="1" width="1" alt="" style="border:0;float:left;margin:0;padding:0;width:1px!important;height:1px!important;" hspace="0" src="https://feeds.feedblitz.com/~/i/967131095/0/baeldung">
<div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/967131095/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/967131095/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f07%2fJava-Featured-12-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/967131095/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/967131095/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/967131095/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/spring-ai-automemorytools#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/spring-ai-automemorytools/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</content:encoded>
					
					<wfw:commentRss>https://feeds.feedblitz.com/~/967131095/0/baeldung/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<webfeeds:featuredImage>https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-150x150.jpg</webfeeds:featuredImage></item>
<item>
<feedburner:origLink>https://www.baeldung.com/spring-ai-agent2agent-protocol</feedburner:origLink>
		<title>Exploring Agent2Agent Protocol (A2A) With Spring AI</title>
		<link>https://feeds.feedblitz.com/~/967127630/0/baeldung</link>
					<comments>https://feeds.feedblitz.com/~/967127630/0/baeldung#respond</comments>
		
		<dc:creator><![CDATA[Hardik Singh Behl]]></dc:creator>
		<pubDate>Fri, 07 Aug 2026 02:59:48 +0000</pubDate>
				<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Spring AI ChatClient]]></category>
		<guid isPermaLink="false">https://www.baeldung.com/?p=204564</guid>
					<description><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-13-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="max-width:100% !important;height:auto !important;float: left; margin-right: 5px;" loading="lazy" /><p>Learn how to make your AI agents to discover each other and communicate efficiently using A2A and SpringAI.</p>
<p>The post <a rel="NOFOLLOW" href="https://feeds.feedblitz.com/~/967127630/0/baeldung">Exploring Agent2Agent Protocol (A2A) With Spring AI</a> first appeared on <a rel="NOFOLLOW" href="https://www.baeldung.com">Baeldung</a>.</p><div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/967127630/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/967127630/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f07%2fJava-Featured-13-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/967127630/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/967127630/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/967127630/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/spring-ai-agent2agent-protocol#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/spring-ai-agent2agent-protocol/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</description>
										<content:encoded><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-13-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="float: left; margin-right: 5px;" decoding="async" loading="lazy" srcset="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-13-1024x536.jpg 1024w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-13-300x157.jpg 300w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-13-768x402.jpg 768w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-13-100x52.jpg 100w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-13.jpg 1200w" sizes="auto, (max-width: 580px) 100vw, 580px" /><h2 id="bd-overview" data-id="overview">1. Overview</h2>
<div class="bd-anchor" id="overview"></div>
<p>We&#8217;re increasingly building AI agents that can handle a user&#8217;s entire request on their own. Instead of simply answering questions from what a <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/cs/large-language-models">Large Language Model (LLM)</a> already knows, these agents reason through a problem, break it down into steps, call external tools, and even execute local scripts.</p>
<p>As these requests grow in complexity, packing every capability into a single agent becomes unmanageable. The natural fix is to create smaller agents, each specialized in one job. However, getting these agents to talk to each other is a challenge in itself, as they run as independent services built using different languages, frameworks, and LLMs.</p>
<p>The <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://a2a-protocol.org/">Agent2Agent (A2A) Protocol</a> addresses this issue by defining a standard for agents to discover each other and communicate.</p>
<p>In this tutorial, we&#8217;ll practically explore the A2A protocol by implementing its client and server architecture using <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-ai">Spring AI</a>.</p>
<h2 id="bd-agent2agent-a2a-protocol-101" data-id="agent2agent-a2a-protocol-101">2. Agent2Agent (A2A) Protocol 101</h2>
<div class="bd-anchor" id="agent2agent-a2a-protocol-101"></div>
<p>Before we dive into the implementation, let&#8217;s take a closer look at the protocol and the way two agents interact:</p>
<p>&nbsp;</p>
<a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/wp-content/uploads/2026/08/a2a-protocol-client-server-flow-diagram-1.png"><img loading="lazy" decoding="async" class="alignnone wp-image-204573" src="https://www.baeldung.com/wp-content/uploads/2026/08/a2a-protocol-client-server-flow-diagram-1-300x66.png" alt="" width="1032" height="227" srcset="https://www.baeldung.com/wp-content/uploads/2026/08/a2a-protocol-client-server-flow-diagram-1-300x66.png 300w, https://www.baeldung.com/wp-content/uploads/2026/08/a2a-protocol-client-server-flow-diagram-1-768x170.png 768w, https://www.baeldung.com/wp-content/uploads/2026/08/a2a-protocol-client-server-flow-diagram-1-1536x340.png 1536w, https://www.baeldung.com/wp-content/uploads/2026/08/a2a-protocol-client-server-flow-diagram-1-2048x453.png 2048w, https://www.baeldung.com/wp-content/uploads/2026/08/a2a-protocol-client-server-flow-diagram-1-100x22.png 100w, https://www.baeldung.com/wp-content/uploads/2026/08/a2a-protocol-client-server-flow-diagram-1-600x133.png 600w" sizes="auto, (max-width: 1032px) 100vw, 1032px" /></a>
<p>An agent acting as a server exposes its capabilities to the outside world, while an agent acting as a client consumes them.</p>
<p><strong>Discovery happens through an Agent Card, which is a JSON document that a server publishes at a <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://en.wikipedia.org/wiki/Well-known_URI#:~:text=2019%2D03%2D01-,agent%2Dcard.json,-Details%20for%20an">well-known URL</a></strong>. This card exposes the agent&#8217;s details, including the list of skills it offers. A client agent fetches this card first to learn what a remote agent can do and where to reach it.</p>
<p><strong>The client sends a Message that describes the work to be done in plain natural language. The remote agent turns that message into a Task, processes it, and returns one or more Artifacts that carry the actual response content</strong>.</p>
<p>A2A is a complex and vast topic. We can refer to the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://a2a-protocol.org/latest/specification/">official specification</a> to learn more.</p>
<h2 id="bd-the-project-were-building" data-id="the-project-were-building">3. The Project We&#8217;re Building</h2>
<div class="bd-anchor" id="the-project-were-building"></div>
<p>To see the protocol in action, we&#8217;ll build a job screening system for recruiters:</p>
<p>&nbsp;</p>
<a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/wp-content/uploads/2026/08/a2a-job-screening-architecture-diagram-1-scaled.png"><img loading="lazy" decoding="async" class="alignnone wp-image-204572" src="https://www.baeldung.com/wp-content/uploads/2026/08/a2a-job-screening-architecture-diagram-1-300x188.png" alt="" width="1146" height="718" srcset="https://www.baeldung.com/wp-content/uploads/2026/08/a2a-job-screening-architecture-diagram-1-300x188.png 300w, https://www.baeldung.com/wp-content/uploads/2026/08/a2a-job-screening-architecture-diagram-1-1024x642.png 1024w, https://www.baeldung.com/wp-content/uploads/2026/08/a2a-job-screening-architecture-diagram-1-768x481.png 768w, https://www.baeldung.com/wp-content/uploads/2026/08/a2a-job-screening-architecture-diagram-1-1536x963.png 1536w, https://www.baeldung.com/wp-content/uploads/2026/08/a2a-job-screening-architecture-diagram-1-2048x1283.png 2048w, https://www.baeldung.com/wp-content/uploads/2026/08/a2a-job-screening-architecture-diagram-1-100x63.png 100w, https://www.baeldung.com/wp-content/uploads/2026/08/a2a-job-screening-architecture-diagram-1-600x376.png 600w" sizes="auto, (max-width: 1146px) 100vw, 1146px" /></a>
<p>As we can see, <strong>our system is made up of one orchestrator agent that acts as the A2A client and communicates with three specialized remote agents that act as A2A servers</strong>.</p>
<p>A recruiter submits a candidate&#8217;s details to a single REST endpoint. Behind the scenes, the orchestrator agent will break the request down and delegate each task to a specialized agent. Once every agent responds, the orchestrator will merge the individual verdicts into one short screening summary.</p>
<h2 id="bd-creating-an-a2a-server" data-id="creating-an-a2a-server">4. Creating an A2A Server</h2>
<div class="bd-anchor" id="creating-an-a2a-server"></div>
<p>Since creating A2A servers follows the exact same structure, we&#8217;ll only walk through implementing the skills matcher agent.</p>
<p>The remaining two remote servers differ solely in their tools and prompts. To view the complete implementation of the project, we can refer to the repository backing this tutorial.</p>
<h3 id="bd-1-dependencies-and-llm-configuration" data-id="1-dependencies-and-llm-configuration">4.1. Dependencies and LLM Configuration</h3>
<div class="bd-anchor" id="1-dependencies-and-llm-configuration"></div>
<p>Let&#8217;s start by adding the necessary dependencies to our project&#8217;s <em>pom.xml</em> file:</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
    &lt;artifactId&gt;spring-ai-starter-model-openai&lt;/artifactId&gt;
    &lt;version&gt;2.0.0&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springaicommunity&lt;/groupId&gt;
    &lt;artifactId&gt;spring-ai-a2a-server-autoconfigure&lt;/artifactId&gt;
    &lt;version&gt;0.3.0&lt;/version&gt;
&lt;/dependency&gt;</code></pre>
<p>Here, we first import <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-starter-model-openai">Spring AI’s OpenAI starter dependency</a>, which we’ll use to interact with an LLM. Additionally, <strong>we import the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.springaicommunity/spring-ai-a2a-server-autoconfigure">A2A server autoconfigure</a> dependency from the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/spring-ai-community">Spring AI community</a>, which takes care of serving our agent card at startup and handling A2A requests</strong>.</p>
<p>Next, let’s configure our <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://platform.openai.com/api-keys">OpenAI API key</a> and <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://developers.openai.com/api/docs/models">chat model</a> in the <em>application.yaml</em> file:</p>
<pre><code class="language-yaml">spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        model: gpt-5.5</code></pre>
<p>Here, we specify OpenAI&#8217;s <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://openai.com/index/introducing-gpt-5-5/">GPT 5.5</a> model using the <em>gpt-5.5</em> model ID. Alternatively, we can use a different chat model, as the specific AI model or provider is irrelevant for this demonstration.</p>
<p><strong>With these two properties set, Spring AI automatically creates a bean of type <em>ChatClient.Builder</em></strong>, which we&#8217;ll use in the upcoming section.</p>
<h3 id="bd-2-creating-a-tool-and-registering-it-with-chatclient" data-id="2-creating-a-tool-and-registering-it-with-chatclient">4.2. Creating a Tool and Registering It With <em>ChatClient</em></h3>
<div class="bd-anchor" id="2-creating-a-tool-and-registering-it-with-chatclient"></div>
<p>Next, let&#8217;s configure the actual capability that our A2A server offers. We&#8217;ll create a <em>SkillsMatcherTools</em> class and define a tool that evaluates a candidate&#8217;s skills:</p>
<pre class="add-horizontal-scrollbar"><code class="language-java">@Tool(
  name = "match-skills",
  description = "Compares a candidate's skills against a job's required skills and returns a fit score"
)
SkillsMatchResult matchSkills(
  @ToolParam(description = "Candidate skills, comma-separated") String candidateSkills,
  @ToolParam(description = "Required job skills, comma-separated") String requiredSkills
) {
    // ... rudimentary implementation
}
record SkillsMatchResult(
    int score,
    Verdict verdict,
    Set&lt;String&gt; matchedSkills,
    Set&lt;String&gt; missingSkills
) {}
enum Verdict {
    STRONG_MATCH,
    PARTIAL_MATCH,
    WEAK_MATCH
}</code></pre>
<p><strong>We annotate our method with the <em>@Tool</em> annotation and give it an explicit <em>name</em> along with a short <em>description</em>. Both of these values help the AI model decide if and when to invoke this tool</strong>. Similarly, we describe both method parameters with the <em>@ToolParam</em> annotation so that the LLM knows the format we expect the inputs to be in.</p>
<p>We&#8217;ve deliberately omitted the method implementation here, since it&#8217;s not important to our A2A understanding.</p>
<p>Next, let&#8217;s create a <em>ChatClient</em> bean and register our tool with it:</p>
<pre><code class="language-java">@Bean
ChatClient chatClient(
  ChatClient.Builder chatClientBuilder,
  SkillsMatcherTools skillsMatcherTools
) {
    return chatClientBuilder
      .defaultSystem("""
        You are a skills-matching assistant for recruiters.
        Use the match-skills tool to compare a candidate's skills
        against a job's required skills, then summarize the result.
        """)
      .defaultTools(skillsMatcherTools)
      .build();
}</code></pre>
<p>Here, we use the <em>ChatClient.Builder</em> bean that Spring AI configures for us, along with the <em>SkillsMatcherTools</em> bean we defined above, to create a <em>ChatClient</em> bean. <strong>This class acts as our main entry point for interacting with the configured LLM</strong>.</p>
<p>Additionally, we define a system prompt defining the agent&#8217;s role, and then register our tool class using the <em>defaultTools()</em> method. This enables the model to call our tool method when it receives a matching request.</p>
<h3 id="bd-3-defining-an-agentexecutor-to-handle-a2a-requests" data-id="3-defining-an-agentexecutor-to-handle-a2a-requests">4.3. Defining an <em>AgentExecutor</em> to Handle A2A Requests</h3>
<div class="bd-anchor" id="3-defining-an-agentexecutor-to-handle-a2a-requests"></div>
<p>The <em>ChatClient</em> bean we&#8217;ve defined can only be used by components within our own application. To let other agents send us tasks, we need to define an <em>AgentExecutor</em> bean that handles incoming A2A requests:</p>
<pre class="add-horizontal-scrollbar"><code class="language-java">@Bean
AgentExecutor agentExecutor(ChatClient chatClient) {
    return new DefaultAgentExecutor(chatClient, (client, requestContext) -&gt; {
        String userMessage = DefaultAgentExecutor.extractTextFromMessage(requestContext.getMessage());
        return client
          .prompt()
          .user(userMessage)
          .call()
          .content();
    });
}</code></pre>
<p>Here, we use the <em>DefaultAgentExecutor</em> class, passing it our <em>ChatClient</em> bean along with a handler function that defines how we want to respond. In the handler function, we extract the plain text of the incoming message and pass it to our LLM as a user prompt. <strong>The response from our <em>chatClient</em> bean is automatically wrapped into an artifact and sent back to the calling agent</strong>.</p>
<h3 id="bd-4-describing-our-agent-using-an-agentcard" data-id="4-describing-our-agent-using-an-agentcard">4.4. Describing Our Agent Using an <em>AgentCard</em></h3>
<div class="bd-anchor" id="4-describing-our-agent-using-an-agentcard"></div>
<p>Finally, we need to expose an Agent Card that accurately describes our agent:</p>
<pre><code class="language-java">@Bean
AgentCard agentCard(
  @Value("${server.host}") String host,
  @Value("${server.port}") int port
) {
    return new AgentCard.Builder()
      .name("Skills Matcher Agent")
      .description("Evaluates how well a candidate's skills match a job's required skills")
      .url(String.format("http://%s:%d/", host, port))
      .version("1.0.0")
      .capabilities(new AgentCapabilities
        .Builder()
        .streaming(false)
        .build())
      .defaultInputModes(List.of("text"))
      .defaultOutputModes(List.of("text"))
      .skills(List.of(new AgentSkill.Builder()
        .id("skills_matching")
        .name("Skills Matching")
        .description("Compares candidate skills to job requirements and scores the fit")
        .tags(List.of("hiring", "recruiting"))
        .build()))
      .protocolVersion("1.0.1")
      .build();
}</code></pre>
<p>Here, we create an <em>AgentCard</em> bean and define the important properties of <em>name</em>, <em>description</em>, and <em>skills</em>. <strong>Client agents rely on these properties to decide whether a remote agent and the capabilities it offers are a good fit for a given task</strong>.</p>
<p>Similarly, we define the <em>url</em> property from the <em>host</em> and <em>port</em> of our running application, telling clients where to reach us. Additionally, we declare that our agent doesn&#8217;t support streaming and it exchanges plain text in both directions.</p>
<p><strong>It&#8217;s worth noting that every property we set above is mandatory</strong>, and the builder rejects a card with any of them missing. With this bean defined, <strong>the auto-configuration serves our agent card at the <em>.well-known/agent-card.json</em> path</strong>.</p>
<h2 id="bd-creating-an-a2a-client" data-id="creating-an-a2a-client">5. Creating an A2A Client</h2>
<div class="bd-anchor" id="creating-an-a2a-client"></div>
<p>With our specialized agents ready, we&#8217;ll build our A2A client, i.e., the job screening orchestrator that delegates the actual evaluation to them.</p>
<h3 id="bd-1-dependencies" data-id="1-dependencies">5.1. Dependencies</h3>
<div class="bd-anchor" id="1-dependencies"></div>
<p><strong>Our orchestrator is a separate application that also talks with an LLM. For this reason, we&#8217;ll need to import a chat model dependency and configure the API key and model properties, just like we did in our A2A server</strong>.</p>
<p>In addition to that, we need to add the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/io.github.a2asdk/a2a-java-sdk-client">A2A Java SDK</a> to our <em>pom.xml</em>:</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;io.github.a2asdk&lt;/groupId&gt;
    &lt;artifactId&gt;a2a-java-sdk-client&lt;/artifactId&gt;
    &lt;version&gt;0.3.3.Final&lt;/version&gt;
&lt;/dependency&gt;</code></pre>
<p>This SDK provides us the classes we need to fetch agent cards, open connections to remote agents, and send them messages.</p>
<p>We should note that we didn&#8217;t explicitly declare this dependency while building our A2A server, as the <em>server autoconfigure</em> dependency brings it in transitively. However, for an agent purely acting as a client, we&#8217;ll need to add this ourselves.</p>
<h3 id="bd-2-discovering-remote-agents-at-startup" data-id="2-discovering-remote-agents-at-startup">5.2. Discovering Remote Agents at Startup</h3>
<div class="bd-anchor" id="2-discovering-remote-agents-at-startup"></div>
<p>Our orchestrator can only delegate work to agents it knows about, so let&#8217;s list their addresses in the <em>application.yaml</em>:</p>
<pre><code class="language-yaml">remote:
  agents:
    urls:
      - http://localhost:8081
      - http://localhost:8082
      - http://localhost:8083</code></pre>
<p>Here, <strong>we configure the base URLs of our remote agents using a custom property</strong>. We need to make sure to update these values if the agents run on a different host or port.</p>
<p>Next, let&#8217;s create an <em>AgentRegistry</em> component that fetches agent cards from these URLs when the application starts:</p>
<pre><code class="language-java">private final Map&lt;String, AgentCard&gt; agentCards = new HashMap&lt;&gt;();
AgentRegistry(@Value("${remote.agents.urls}") List&lt;String&gt; agentUrls) {
    for (String url : agentUrls) {
        String path = new URI(url).getPath();
        AgentCard card = A2A.getAgentCard(url, path + ".well-known/agent-card.json", null);
        agentCards.put(card.name(), card);
    }
}</code></pre>
<p>Here, we iterate over each configured URL inside the constructor and retrieve its agent card from the <em>.well-known/agent-card.json</em> path. <strong>Then, we store the resulting <em>AgentCard</em> instances in an in-memory map keyed by the agent&#8217;s name</strong>.</p>
<p>To expose the fetched agent cards to other components, let&#8217;s add a couple of helper methods to this class:</p>
<pre><code class="language-java">AgentCard get(String agentName) {
    return agentCards.get(agentName);
}
String describeAgents() {
    return agentCards
      .values()
      .stream()
      .map(card -&gt; "- " + card.name() + ": " + card.description())
      .collect(Collectors.joining("\n"));
}</code></pre>
<p>The <em>get()</em> method returns the card of a specific agent by its name. Meanwhile, the <em>describeAgents()</em> method renders the formatted summary of all available remote agents with their descriptions. We&#8217;ll use these helper methods in the upcoming sections to define additional components.</p>
<h3 id="bd-3-communicating-with-remote-agents" data-id="3-communicating-with-remote-agents">5.3. Communicating With Remote Agents</h3>
<div class="bd-anchor" id="3-communicating-with-remote-agents"></div>
<p>Our A2A client is now capable of discovering remote agents at startup. Next, let&#8217;s create a <em>RemoteAgentClient</em> component that actually communicates with these agents:</p>
<pre><code class="language-java">String sendMessage(String agentName, String task) {
    AgentCard agentCard = agentRegistry.get(agentName);
    CompletableFuture&lt;String&gt; response = new CompletableFuture&lt;&gt;();
    BiConsumer&lt;ClientEvent, AgentCard&gt; responseConsumer = (event, card) -&gt; {
        TaskEvent taskEvent = (TaskEvent) event;
        response.complete(taskEvent.getTask()
          .getArtifacts()
          .stream()
          .map(Artifact::parts)
          .map(this::extractText)
          .collect(Collectors.joining("\n")));
    };
    Client client = Client.builder(agentCard)
      .clientConfig(new ClientConfig.Builder()
        .setAcceptedOutputModes(List.of("text"))
        .build())
      .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig())
      .addConsumers(List.of(responseConsumer))
      .streamingErrorHandler(response::completeExceptionally)
      .build();
    Message message = A2A.toUserMessage(task);
    client.sendMessage(message);
    return response.get(60, TimeUnit.SECONDS);
}
private String extractText(List&lt;Part&lt;?&gt;&gt; parts) {
    return parts
      .stream()
      .filter(TextPart.class::isInstance)
      .map(TextPart.class::cast)
      .map(TextPart::getText)
      .collect(Collectors.joining("\n"));
}</code></pre>
<p>Here, we start by fetching the target agent&#8217;s card from our registry, as the SDK uses it to build a client.</p>
<p><strong>Since the SDK returns responses asynchronously, we register a consumer that receives the completed task. Then, it pulls the text out of its artifacts, and passes it to our <em><a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/java-completablefuture">CompletableFuture</a></em> <em>response</em></strong>. Then, we convert the given <em>task</em> into an A2A message, send it to the remote agent, and wait for the <em>response</em> to complete.</p>
<p>Next, let&#8217;s expose this capability as a tool that our orchestrator&#8217;s LLM can call:</p>
<pre><code class="language-java">@Tool(
  name = "send-message-to-agent",
  description = "Sends a task to a remote agent and returns its response."
)
String sendMessageToAgent(
  @ToolParam(description = "Name of the remote agent") String agentName,
  @ToolParam(description = "The task to perform") String task
) {
    return remoteAgentClient.sendMessage(agentName, task);
}</code></pre>
<p>This single tool is all the orchestrator needs to reach all our configured agents. The LLM will decide which agent to contact and what to ask it, simply by filling in the <em>agentName</em> and <em>task</em> parameters.</p>
<h3 id="bd-4-building-the-orchestrator-chatclient" data-id="4-building-the-orchestrator-chatclient">5.4. Building the Orchestrator <em>ChatClient</em></h3>
<div class="bd-anchor" id="4-building-the-orchestrator-chatclient"></div>
<p>With our discovery and communication logic in place, let&#8217;s create the <em>ChatClient</em> bean for our orchestrator agent:</p>
<pre><code class="language-java">@Bean
ChatClient chatClient(
  ChatClient.Builder chatClientBuilder,
  AgentRegistry agentRegistry,
  RemoteAgentTools remoteAgentTools
) {
    return chatClientBuilder
      .defaultSystem("""
        You are a job-screening orchestrator for recruiters.
        You do not evaluate candidates yourself. Instead, you delegate
        to the following remote agents:
        %s
        Once all agents have responded, combine their responses into a short screening summary.
        """.formatted(agentRegistry.describeAgents()))
      .defaultTools(remoteAgentTools)
      .build();
}</code></pre>
<p>In our system prompt, we explicitly instruct the model not to evaluate candidates on its own. Instead, <strong>we inject the list of discovered agents using the <em>describeAgents()</em> method and ask it to delegate the individual evaluations to them</strong>.</p>
<p>Then, we register our tool, which gives the model the capability to actually reach these agents.</p>
<h3 id="bd-5-exposing-a-rest-api" data-id="5-exposing-a-rest-api">5.5. Exposing a REST API</h3>
<div class="bd-anchor" id="5-exposing-a-rest-api"></div>
<p>Finally, let&#8217;s use the orchestrator <em>ChatClient</em> we&#8217;ve defined and expose a REST API endpoint to accept candidate screening requests:</p>
<pre><code class="language-java">@PostMapping("/screenings")
ScreeningResponse screenCandidate(@RequestBody ScreeningRequest screeningRequest) {
    String verdict = chatClient
      .prompt()
      .user(screeningRequest.toString())
      .call()
      .content();
    return new ScreeningResponse(verdict);
}
record ScreeningRequest(
  String name,
  String email,
  String jobTitle,
  String requiredSkills,
  String candidateSkills,
  int expectedSalary
) {}
record ScreeningResponse(
  String verdict
) {}</code></pre>
<p>Our endpoint accepts a <em>ScreeningRequest</em> record holding everything our agents need, namely the candidate&#8217;s identity, the job details, and the expected salary.</p>
<p>We simply pass the record&#8217;s string representation to the model as a user prompt and return the resulting summary as a <em>ScreeningResponse</em>. <strong>This way, our orchestrator client receives the full candidate details and distributes the relevant data to each of our specialized agents</strong>.</p>
<h2 id="bd-testing-our-implementation" data-id="testing-our-implementation">6. Testing Our Implementation</h2>
<div class="bd-anchor" id="testing-our-implementation"></div>
<p>With our architecture implemented, let&#8217;s start all our agents and test the job screening flow.</p>
<p>We&#8217;ll use the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/httpie-http-client-command-line">HTTPie</a> CLI to invoke our screening endpoint:</p>
<pre><code class="language-bash">http POST :8080/screenings \
  name="John Doe" \
  email="john.doe@baeldung.com" \
  jobTitle="Backend Developer" \
  requiredSkills="Java, Spring Boot, AWS, Kafka" \
  candidateSkills="Java, Spring Boot, Azure, Kafka" \
  expectedSalary:=110000 \
  | jq -r '.verdict'</code></pre>
<p>Here, we submit sample data for a candidate and <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/linux/anonymous-named-pipes#what-is-a-pipe">pipe</a> the response through <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/linux/jq-command-json"><em>jq</em></a> to print the <em>verdict</em> as readable text.</p>
<p>Let’s see what we get as a response:</p>
<pre class="add-horizontal-scrollbar"><code class="language-bash">Screening summary for John Doe (Backend Developer):
- Salary: Expected salary of $110,000 is within budget.
- Background check: Clear; no relevant flags found.
- Skills match: Strong match, 75% fit. Only missing AWS experience, though Azure experience may be transferable.
Overall: John Doe appears to be a good candidate to proceed with, with follow-up recommended on AWS/cloud experience.</code></pre>
<p>As we can see, <strong>the orchestrator delegated the request to all three remote agents and consolidated their individual verdicts into a single summary</strong>.</p>
<h2 id="bd-conclusion" data-id="conclusion">7. Conclusion</h2>
<div class="bd-anchor" id="conclusion"></div>
<p>In this article, we&#8217;ve understood what the Agent2Agent (A2A) protocol is and practically implemented it using Spring AI.</p>
<p>We started by building an A2A server, exposing its capability through an agent card. Next, we built an A2A client that acts as an orchestrator, dynamically discovering remote agents and delegating tasks to them. Finally, we tested the complete flow of our implementation and confirmed that our orchestrator combines the responses of all the specialized agents into one screening summary.</p>
<p>As always, all the code examples used in this article are available <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/eugenp/tutorials/tree/master/spring-ai-modules/spring-ai-a2a">over on GitHub</a>.</p><p>The post <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-ai-agent2agent-protocol">Exploring Agent2Agent Protocol (A2A) With Spring AI</a> first appeared on <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com">Baeldung</a>.</p><Img align="left" border="0" height="1" width="1" alt="" style="border:0;float:left;margin:0;padding:0;width:1px!important;height:1px!important;" hspace="0" src="https://feeds.feedblitz.com/~/i/967127630/0/baeldung">
<div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/967127630/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/967127630/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f07%2fJava-Featured-13-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/967127630/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/967127630/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/967127630/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/spring-ai-agent2agent-protocol#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/spring-ai-agent2agent-protocol/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</content:encoded>
					
					<wfw:commentRss>https://feeds.feedblitz.com/~/967127630/0/baeldung/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<webfeeds:featuredImage>https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-13-150x150.jpg</webfeeds:featuredImage></item>
<item>
<feedburner:origLink>https://www.baeldung.com/java-weekly-657</feedburner:origLink>
		<title>Java Weekly, Issue 657</title>
		<link>https://feeds.feedblitz.com/~/965486156/0/baeldung</link>
					<comments>https://feeds.feedblitz.com/~/965486156/0/baeldung#respond</comments>
		
		<dc:creator><![CDATA[baeldung]]></dc:creator>
		<pubDate>Sat, 01 Aug 2026 15:56:58 +0000</pubDate>
				<category><![CDATA[Weekly Review]]></category>
		<category><![CDATA[no-ads]]></category>
		<category><![CDATA[no-after-post]]></category>
		<category><![CDATA[no-before-post]]></category>
		<category><![CDATA[no-optins]]></category>
		<guid isPermaLink="false">https://www.baeldung.com/?p=204552</guid>
					<description><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2016/10/social-Weekly-Reviews-4.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="max-width:100% !important;height:auto !important;float: left; margin-right: 5px;" loading="lazy" /><p>A Java movie? Cool. And Spring Cloud Contract has a new home.</p>
<p>The post <a rel="NOFOLLOW" href="https://feeds.feedblitz.com/~/965486156/0/baeldung">Java Weekly, Issue 657</a> first appeared on <a rel="NOFOLLOW" href="https://www.baeldung.com">Baeldung</a>.</p><div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/965486156/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/965486156/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2016%2f10%2fsocial-Weekly-Reviews-4.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/965486156/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/965486156/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/965486156/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/java-weekly-657#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/java-weekly-657/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</description>
										<content:encoded><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2016/10/social-Weekly-Reviews-4.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="float: left; margin-right: 5px;" decoding="async" loading="lazy" srcset="https://www.baeldung.com/wp-content/uploads/2016/10/social-Weekly-Reviews-4.jpg 952w, https://www.baeldung.com/wp-content/uploads/2016/10/social-Weekly-Reviews-4-300x157.jpg 300w, https://www.baeldung.com/wp-content/uploads/2016/10/social-Weekly-Reviews-4-768x402.jpg 768w" sizes="auto, (max-width: 580px) 100vw, 580px" /><h2 style="text-align: left;" id="bd-spring-and-java" data-id="spring-and-java">1.<strong> Spring and Java</strong></h2>
<div class="bd-anchor" id="spring-and-java"></div>
<p><strong><a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.infoq.com/articles/self-building-agent-langchain4j/">&gt;&gt; The Self-Building Agent: a LangChain4j Experiment</a></strong> [<span style="color: #993300;">infoq.com</span>]</p>
<p>A interesting reveral of the typical agent tutorial by pointing an LLM at the LangChain4j documentation and having it <strong>design an agent</strong>. Meta yes, and worth a quick read.</p>
<h4><strong>Also worth reading:</strong></h4>
<ul>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://quarkus.io/blog/to-cache-or-not-to-cache-virtual-threads/" target="_blank" rel="noopener"><strong>To Cache or Not to Cache Virtual Threads</strong></a> [<span style="color: #800000;">quarkus.io</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://inside.java/2026/07/25/design-java-mcp-tool/" target="_blank" rel="noopener"><strong>Pairing In-Process and Hosted Embeddings for Java MCP Tool Development</strong></a> [<span style="color: #800000;">inside.java</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://foojay.io/today/new-between-quarters-security-updates-for-java-what-cspus-mean-for-your-release-pipeline/" target="_blank" rel="noopener"><strong>New Between-Quarters Security Updates for Java: What CSPUs Mean for Your Release Pipeline</strong></a> [<span style="color: #800000;">foojay.io</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://blog.jetbrains.com/idea/2026/07/reverse-engineering-with-hibernate-7-4-and-intellij-idea/" target="_blank" rel="noopener"><strong>Reverse Engineering with Hibernate 7.4 and IntelliJ IDEA</strong></a> [<span style="color: #800000;">jetbrains.com</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.wimdeblauwe.com/blog/2026/07/27/writing-a-thymeleaf-component-library/" target="_blank" rel="noopener"><strong>Writing a Thymeleaf component library</strong></a> [<span style="color: #800000;">wimdeblauwe.com</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://inside.java/2026/07/28/quality-heads-up/" target="_blank" rel="noopener"><strong>Quality Outreach Heads-up &#8211; JDK 28: jlink cacerts plugin</strong></a> [<span style="color: #800000;">inside.java</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://blog.jetbrains.com/research/2026/07/kotlinllm-open-source/" target="_blank" rel="noopener"><strong>KotlinLLM is Going Open Source</strong></a> [<span style="color: #800000;">jetbrains.com</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://blog.frankel.ch/rfc-9880-iot-validation-compiler/" target="_blank" rel="noopener"><strong>RFC 9880 and the IoT Validation Problem</strong></a> [<span style="color: #800000;">frankel.ch</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://foojay.io/today/ai-found-the-bugs-whos-patching-your-eol-java-code/" target="_blank" rel="noopener"><strong>AI Found the Bugs. Who&#8217;s Patching Your EOL Java Code?</strong></a> [<span style="color: #800000;">foojay.io</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://blog.jetbrains.com/kotlin/2026/07/secure-your-apis-oauth2-and-jwt-for-beginners/" target="_blank" rel="noopener"><strong>Secure Your APIs: OAuth2 and JWT for Beginners</strong></a> [<span style="color: #800000;">jetbrains.com</span>]</li>
</ul>
<h4><strong>Webinars and presentations:</strong></h4>
<ul>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://inside.java/2026/07/23/podcast-063/" target="_blank" rel="noopener"><strong>Episode 63 &#8220;AI Solutions with Spring AI 2.0&#8221; [I/O]</strong></a> [<span style="color: #800000;">inside.java</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://spring.io/blog/2026/07/23/a-bootiful-podcast-billy-korando" target="_blank" rel="noopener"><strong>A Bootiful Podcast: Java Developer Advocate Billy Korando on Java 27 and Beyond</strong></a> [<span style="color: #800000;">spring.io</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://spring.io/blog/2026/07/27/spring-office-hours-podcast-S5E19" target="_blank" rel="noopener"><strong>Spring Office Hours Podcast: S5E19 &#8211; Docker, Compose, Testcontainers, Oh My!</strong></a> [<span style="color: #800000;">spring.io</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.infoq.com/presentations/kotlin-serverless/" target="_blank" rel="noopener"><strong>Clean Architecture for Serverless: Business Logic You Can Take Anywhere</strong></a> [<span style="color: #800000;">infoq.com</span>]</li>
</ul>
<h4><strong>Time to upgrade:</strong></h4>
<ul>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://quarkus.io/blog/quarkus-3-37-4-released/" target="_blank" rel="noopener"><strong>Quarkus 3.37.4</strong></a> and <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://quarkus.io/blog/quarkus-3-33-3-released/" target="_blank" rel="noopener"><strong>3.33.3</strong></a> [<span style="color: #800000;">quarkus.io</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/graalvm/graalvm-ce-builds/releases/tag/graal-25.2.4" target="_blank" rel="noopener"><strong>GraalVM Community 25 Innovation 2</strong></a> [<span style="color: #800000;">github.com/graalvm</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/Netflix/zuul/releases/tag/v4.1.3" target="_blank" rel="noopener"><strong>Zuul 4.1.3</strong></a> and <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/Netflix/zuul/releases/tag/v4.1.2" target="_blank" rel="noopener"><strong>4.1.2</strong></a> [<span style="color: #800000;">github.com/Netflix</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/micronaut-projects/micronaut-core/releases/tag/v5.0.7" target="_blank" rel="noopener"><strong>Micronaut Core 5.0.7</strong></a> [<span style="color: #800000;">github.com/micronaut-projects</span>]</li>
</ul>
<h2 style="text-align: left;" id="bd-technical-amp-musings" data-id="technical-amp-musings">2.<strong> Technical &amp; Musings</strong></h2>
<div class="bd-anchor" id="technical-amp-musings"></div>
<p><strong><a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://martinfowler.com/articles/orchestrator-tax.html">&gt;&gt; The Orchestrator&#8217;s Tax</a></strong> [<span style="color: #993300;">martinfowler.com</span>]</p>
<p>Insight into <strong>multi-agent coding setups</strong>. Exploratory rather than prescriptive, and better for it.</p>
<h4><strong>Also worth reading:</strong></h4>
<ul>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://blog.christianposta.com/credential-brokering-patterns-for-ai-agent-egress/" target="_blank" rel="noopener"><strong>Credential Brokering Patterns for AI Agents Part 1: Don&#8217;t Give the Agent the Keys</strong></a> [<span style="color: #800000;">christianposta.com</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://blog.scottlogic.com/2026/07/27/rise-of-open-weights.html" target="_blank" rel="noopener"><strong>The Rise of Open Weights; And the fall of commercial AI?</strong></a> [<span style="color: #800000;">scottlogic.com</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://event-driven.io/en/throw-result-or-neither/" target="_blank" rel="noopener"><strong>Throw, Result, or neither?</strong></a> [<span style="color: #800000;">event-driven.io</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://lucumr.pocoo.org/2026/7/24/codeberg-divides/" target="_blank" rel="noopener"><strong>Codeberg Divides</strong></a> [<span style="color: #800000;">lucumr.pocoo.org</span>]</li>
<li><strong>&gt;&gt;</strong> <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://blog.scottlogic.com/2026/07/27/shifting-readiness-left-what-ai-can-and-cant-do-for-organisational-readiness.html" target="_blank" rel="noopener"><strong>Shifting readiness left: What AI can (and can&#8217;t) do for organisational readiness</strong></a> [<span style="color: #800000;">scottlogic.com</span>]</li>
</ul>
<h2 style="text-align: left;" id="bd-pick-of-the-week" data-id="pick-of-the-week">3.<strong> Pick of the Week</strong></h2>
<div class="bd-anchor" id="pick-of-the-week"></div>
<p><strong><a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://seths.blog/2009/04/first-ten/">&gt;&gt; First, ten</a></strong> [<strong><span style="color: #993300;">seths.blog</span></strong>]</p><p>The post <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/java-weekly-657">Java Weekly, Issue 657</a> first appeared on <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com">Baeldung</a>.</p><Img align="left" border="0" height="1" width="1" alt="" style="border:0;float:left;margin:0;padding:0;width:1px!important;height:1px!important;" hspace="0" src="https://feeds.feedblitz.com/~/i/965486156/0/baeldung">
<div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/965486156/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/965486156/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2016%2f10%2fsocial-Weekly-Reviews-4.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/965486156/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/965486156/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/965486156/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/java-weekly-657#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/java-weekly-657/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</content:encoded>
					
					<wfw:commentRss>https://feeds.feedblitz.com/~/965486156/0/baeldung/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<webfeeds:featuredImage>https://www.baeldung.com/wp-content/uploads/2016/10/social-Weekly-Reviews-4-150x150.jpg</webfeeds:featuredImage></item>
<item>
<feedburner:origLink>https://www.baeldung.com/spark-dataframe-empty</feedburner:origLink>
		<title>Check if Spark Dataframe Is Empty</title>
		<link>https://feeds.feedblitz.com/~/965089607/0/baeldung</link>
					<comments>https://feeds.feedblitz.com/~/965089607/0/baeldung#respond</comments>
		
		<dc:creator><![CDATA[Yadu Krishnan]]></dc:creator>
		<pubDate>Fri, 31 Jul 2026 18:24:51 +0000</pubDate>
				<category><![CDATA[Data]]></category>
		<category><![CDATA[Spark]]></category>
		<guid isPermaLink="false">https://www.baeldung.com/?p=204544</guid>
					<description><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-13-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="max-width:100% !important;height:auto !important;float: left; margin-right: 5px;" loading="lazy" /><p>Learn how to check whether a Spark DataFrame is empty to improve performance, especially when working with large datasets.</p>
<p>The post <a rel="NOFOLLOW" href="https://feeds.feedblitz.com/~/965089607/0/baeldung">Check if Spark Dataframe Is Empty</a> first appeared on <a rel="NOFOLLOW" href="https://www.baeldung.com">Baeldung</a>.</p><div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/965089607/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/965089607/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f07%2fJava-Featured-13-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/965089607/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/965089607/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/965089607/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/spark-dataframe-empty#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/spark-dataframe-empty/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</description>
										<content:encoded><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-13-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="float: left; margin-right: 5px;" decoding="async" loading="lazy" srcset="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-13-1024x536.jpg 1024w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-13-300x157.jpg 300w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-13-768x402.jpg 768w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-13-100x52.jpg 100w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-13.jpg 1200w" sizes="auto, (max-width: 580px) 100vw, 580px" /><h2 id="bd-introduction" data-id="introduction">1. Introduction</h2>
<div class="bd-anchor" id="introduction"></div>
<p>When working with <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/apache-spark">Apache Spark</a>, we often encounter situations where a <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spark-dataframes">DataFrame</a> becomes empty after applying transformations, filters, or joins.</p>
<p><strong>Before triggering expensive actions or saving data to external storage, it&#8217;s a best practice to verify if the DataFrame contains any rows</strong>. While this may seem like a trivial check, different approaches trigger different Spark actions and can have a noticeable impact on performance, especially for large datasets.</p>
<p>In this tutorial, we&#8217;ll explore different ways to check whether a Spark DataFrame is empty.</p>
<h2 id="bd-setup" data-id="setup">2. Setup</h2>
<div class="bd-anchor" id="setup"></div>
<p>First, let&#8217;s create the following sample DataFrame:</p>
<pre><code class="language-java">public static Dataset getDataFrame(SparkSession spark) {
    List&lt;Row&gt; players = List.of(
      RowFactory.create(1, "Messi", "Argentina"),
      RowFactory.create(2, "Ronaldo", "Portugal"),
      RowFactory.create(3, "Mbappe", "France"));
    return spark.createDataFrame(players, PLAYER_SCHEMA);
}</code></pre>
<p>We&#8217;ll use this DataFrame throughout the examples.</p>
<h2 id="bd-using-the-isempty-method" data-id="using-the-isempty-method">3. Using the <em>isEmpty()</em> method</h2>
<div class="bd-anchor" id="using-the-isempty-method"></div>
<p><strong>Since Spark 2.4.0, the easiest way to check whether a Dataset is empty is by using the <em>isEmpty()</em> method</strong>.</p>
<p>Let&#8217;s create an empty DataFrame by filtering for a country that doesn&#8217;t exist in the dataset.</p>
<pre><code class="language-java">Dataset&lt;Row&gt; englandPlayers = allPlayers.filter(col("country").equalTo("England"));
Assertions.assertTrue(englandPlayers.isEmpty());
</code></pre>
<p>Since there are no England players in the dataframe, the filter returns an empty dataframe and <em>isEmpty()</em> returns true.</p>
<h2 id="bd-using-the-count-method" data-id="using-the-count-method">4. Using the <em>count()</em> method</h2>
<div class="bd-anchor" id="using-the-count-method"></div>
<p>Another way to check for emptiness is by counting the number of rows in a DataFrame and checking if it&#8217;s zero:</p>
<pre><code class="language-java">Dataset&lt;Row&gt; englandPlayers = allPlayers.filter(col("country").equalTo("England"));
Assertions.assertEquals(0, englandPlayers.count());</code></pre>
<p>Here, we invoked <em>count()</em> and verified that the number of rows is 0.</p>
<h2 id="bd-using-the-takeaslist-method" data-id="using-the-takeaslist-method">5. Using the <em>takeAsList()</em> Method</h2>
<div class="bd-anchor" id="using-the-takeaslist-method"></div>
<p>Another approach is to retrieve at most one row using <em>takeAsList():</em></p>
<pre><code class="language-">Dataset&lt;Row&gt; englandPlayers = allPlayers.filter(col("country").equalTo("England"));
Assertions.assertEquals(0, englandPlayers.takeAsList(1).size());</code></pre>
<p>Here we called the <em>takeAsList(1)</em>, which returns at most one row. We can then verify that the returned list is empty by checking whether its size is 0. <strong>Since only one row is requested, Spark can stop processing once it finds the first matching row</strong>, making this approach more efficient than counting every row.</p>
<p>Alternatively, we can also check if the list is empty instead of <em>size(</em>):</p>
<pre><code class="language-">Assertions.assertTrue(englandPlayers.takeAsList(1).isEmpty());</code></pre>
<h2 id="bd-comparing-methods-for-performance" data-id="comparing-methods-for-performance">6. Comparing Methods for Performance</h2>
<div class="bd-anchor" id="comparing-methods-for-performance"></div>
<p>Although all of these approaches can determine whether a DataFrame is empty, they don&#8217;t have the same performance characteristics.The <em>isEmpty()</em> method is the recommended approach because it&#8217;s specifically designed for this purpose. Internally, <strong>Spark only needs to determine whether at least one row exists, allowing it to stop processing as soon as a row is found</strong>.</p>
<p>Similarly, <em>takeAsList(1)</em> requests only a single row. Once Spark retrieves the first row, it can terminate the scan, making this approach much more efficient than counting every row. In contrast, <em>count()</em> computes the total number of rows in the DataFrame. Since Spark must process the entire dataset to produce an exact count, this approach is considerably more expensive for large DataFrames.</p>
<p>In general, we should prefer <em>isEmpty()</em> whenever it&#8217;s available. If we&#8217;re working with an older Spark version that doesn&#8217;t support it, <em>takeAsList(1).isEmpty()</em> is a good alternative. We should reserve <em>count()</em> for situations where we also need the total number of rows.</p>
<h2 id="bd-conclusion" data-id="conclusion">7. Conclusion</h2>
<div class="bd-anchor" id="conclusion"></div>
<p>In this article, we explored several ways to determine whether a Spark DataFrame is empty. We started with the <em>isEmpty()</em> method, which is the simplest and most expressive solution, and then looked at alternatives based on <em>count()</em> and <em>takeAsList()</em>.</p>
<p><strong>In most situations, <em>isEmpty()</em> is the preferred choice because it&#8217;s both expressive and efficient</strong>. For older Spark versions, <em>takeAsList(1).isEmpty()</em> provides a practical alternative, while <em>count()</em> is best reserved for cases where the total number of rows is also needed.</p>
<p>As always, the sample code used in this article is available <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/eugenp/tutorials/tree/master/apache-spark-2">over on GitHub</a>.</p><p>The post <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spark-dataframe-empty">Check if Spark Dataframe Is Empty</a> first appeared on <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com">Baeldung</a>.</p><Img align="left" border="0" height="1" width="1" alt="" style="border:0;float:left;margin:0;padding:0;width:1px!important;height:1px!important;" hspace="0" src="https://feeds.feedblitz.com/~/i/965089607/0/baeldung">
<div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/965089607/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/965089607/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f07%2fJava-Featured-13-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/965089607/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/965089607/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/965089607/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/spark-dataframe-empty#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/spark-dataframe-empty/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</content:encoded>
					
					<wfw:commentRss>https://feeds.feedblitz.com/~/965089607/0/baeldung/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<webfeeds:featuredImage>https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-13-150x150.jpg</webfeeds:featuredImage></item>
<item>
<feedburner:origLink>https://www.baeldung.com/apache-camel-docling-langchain4j</feedburner:origLink>
		<title>Building Intelligent Document Processing with Apache Camel, Docling and LangChain4j</title>
		<link>https://feeds.feedblitz.com/~/964749371/0/baeldung</link>
					<comments>https://feeds.feedblitz.com/~/964749371/0/baeldung#respond</comments>
		
		<dc:creator><![CDATA[Olayemi Michael]]></dc:creator>
		<pubDate>Thu, 30 Jul 2026 23:35:25 +0000</pubDate>
				<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Apache Camel]]></category>
		<category><![CDATA[LangChain]]></category>
		<guid isPermaLink="false">https://www.baeldung.com/apache-camel-docling-langchain4j</guid>
					<description><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-14-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="Two co-workers; the one on the left is holding up a magnifying glass to an artistic representation of files and folders. The one on the right is looking at a projection of some Java code. In between them there is an office plant. To the left of the image are the words &quot;Java on Baeldung&quot;" style="max-width:100% !important;height:auto !important;float: left; margin-right: 5px;" loading="lazy" /><p>Build an AI-powered document processing system using Apache Camel, Docling, and LangChain4j.</p>
<p>The post <a rel="NOFOLLOW" href="https://feeds.feedblitz.com/~/964749371/0/baeldung">Building Intelligent Document Processing with Apache Camel, Docling and LangChain4j</a> first appeared on <a rel="NOFOLLOW" href="https://www.baeldung.com">Baeldung</a>.</p><div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/964749371/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/964749371/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f07%2fJava-Featured-14-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/964749371/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/964749371/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/964749371/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/apache-camel-docling-langchain4j#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/apache-camel-docling-langchain4j/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</description>
										<content:encoded><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-14-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="Two co-workers; the one on the left is holding up a magnifying glass to an artistic representation of files and folders. The one on the right is looking at a projection of some Java code. In between them there is an office plant. To the left of the image are the words &quot;Java on Baeldung&quot;" style="float: left; margin-right: 5px;" decoding="async" loading="lazy" srcset="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-14-1024x536.jpg 1024w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-14-300x157.jpg 300w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-14-768x402.jpg 768w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-14-100x52.jpg 100w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-14.jpg 1200w" sizes="auto, (max-width: 580px) 100vw, 580px" /><h2 id="bd-overview" data-id="overview">1. Overview</h2>
<div class="bd-anchor" id="overview"></div>
<p>An LLM can answer questions about documents&#8217; content when they are in the context window. However, many real-world documents are stored in formats such as PDF, which may be difficult for AI applications to process directly. Docling converts these documents into a structured format that can be efficiently parsed.</p>
<p>In this tutorial, we&#8217;ll build a document processing system using <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/apache-camel-intro">Apache Camel</a>, Docling, and <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/java-langchain-basics">LangChain4j</a>. Also, we&#8217;ll define a simple question-answering API using an embedded Undertow server.</p>
<h2 id="bd-problem-statement" data-id="problem-statement">2. Problem Statement</h2>
<div class="bd-anchor" id="problem-statement"></div>
<p>Many real-world documents contain structural information such as headings, tables, and images, which is difficult to preserve during text extraction. As a result, AI pipelines may lose valuable context, reducing the quality of retrieval and accuracy of generated responses.</p>
<p><strong>A structured representation helps address this problem by preserving the document&#8217;s organization and relationships</strong>. Structured formats such as Markdown, HTML, and JSON retain much of the document&#8217;s semantic structure, making them easier to process and interpret.</p>
<p><strong>IBM Docling bridges this gap by converting unstructured or semi-structured documents into structured representations that downstream AI applications can efficiently consume. </strong>We can store the generated Markdown on disk. Later, an application can load it and include it in the prompt for the LLM. This avoids converting the same document for every request the application processes.</p>
<h2 id="bd-project-setup" data-id="project-setup">3. Project Setup</h2>
<div class="bd-anchor" id="project-setup"></div>
<p>Let&#8217;s bootstrap a Java project using Apache Camel and LangChain4j.</p>
<h3 id="bd-1-maven-dependencies" data-id="1-maven-dependencies">3.1. Maven Dependencies</h3>
<div class="bd-anchor" id="1-maven-dependencies"></div>
<p>First, let&#8217;s add the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.apache.camel/camel-core"><em>camel-core</em></a>, <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.apache.camel/camel-main"><em>camel-main</em></a>, and <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/dev.langchain4j/langchain4j"><em>langchain4j</em></a> dependencies to our <em>pom.xml</em>:</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.camel&lt;/groupId&gt;
    &lt;artifactId&gt;camel-core&lt;/artifactId&gt;
    &lt;version&gt;4.20.0&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.apache.camel&lt;/groupId&gt;
    &lt;artifactId&gt;camel-main&lt;/artifactId&gt;
    &lt;version&gt;4.20.0&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;
    &lt;artifactId&gt;langchain4j&lt;/artifactId&gt;
    &lt;version&gt;1.11.8&lt;/version&gt;
&lt;/dependency&gt;</code></pre>
<p>The <em>camel-core</em> dependency provides classes such as <em>RouteBuilder</em> for defining a camel route using the Java DSL,<em> camel-main</em> provides a lightweight runtime for standalone Camel applications, and <em>langchain4j</em> provides the core APIs for interacting with LLMs.</p>
<p>Next, let&#8217;s add <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.apache.camel/camel-docling"><em>camel-docling</em></a>, <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.apache.camel/camel-langchain4j-chat"><em>camel-langchain4j-chat</em></a>, and <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/dev.langchain4j/langchain4j-open-ai"><em>langchain4j-open-ai</em></a> dependencies to our <em>pom.xml</em>:</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.camel&lt;/groupId&gt;
    &lt;artifactId&gt;camel-docling&lt;/artifactId&gt;
    &lt;version&gt;4.20.0&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.apache.camel&lt;/groupId&gt;
    &lt;artifactId&gt;camel-langchain4j-chat&lt;/artifactId&gt;
    &lt;version&gt;4.20.0&lt;/version&gt;
&lt;/dependency&gt;</code></pre>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;
    &lt;artifactId&gt;langchain4j-open-ai&lt;/artifactId&gt;
    &lt;version&gt;1.11.8&lt;/version&gt;
&lt;/dependency&gt;</code></pre>
<p>Here, <em>camel-docling</em> integrates Apache Camel with a Docling server, allowing routes to submit documents for conversion into Docling&#8217;s structured document representation. Also, <em>camel-langchain4j-chat</em> enables Camel routes to send prompts to a LangChain4j chat model and process the generated response. <strong>The <em>langchain4j-open-ai</em> dependency provides the implementation needed to connect LangChain4j to OpenAI-compatible chat models</strong>.</p>
<p>All Apache Camel dependencies, including <em>camel-docling, </em>use the same version to ensure compatibility and avoid unexpected runtime errors.</p>
<p>Let&#8217;s also add the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.apache.camel/camel-undertow"><em>camel-undertow</em></a> dependency to our <em>pom.xml</em>:</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.camel&lt;/groupId&gt;
    &lt;artifactId&gt;camel-undertow&lt;/artifactId&gt;
    &lt;version&gt;4.20.0&lt;/version&gt;
&lt;/dependency&gt;</code></pre>
<p>It enables Camel to expose HTTP endpoints using an embedded <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/jboss-undertow">Undertow server</a>.</p>
<p>Finally, let&#8217;s create a directory named <em>document</em> in the project&#8217;s root directory and place the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/eugenp/tutorials/tree/master/libraries-llms-2/document/policies.pdf"><em>policies.pdf</em> </a>file inside it.</p>
<h3 id="bd-2-docker-compose-file" data-id="2-docker-compose-file">3.2. Docker Compose File</h3>
<div class="bd-anchor" id="2-docker-compose-file"></div>
<p>The easiest way to run Docling locally is by using <em>docling-serve</em>, which exposes Docling&#8217;s document conversion capabilities through a REST API. For simplicity, we run <em>docling-serve</em> in a Docker container.</p>
<p>First, let&#8217;s create a new file <em>docker-compose.yml </em>in our project root directory and add the following configuration:</p>
<pre><code class="language-yaml">services:
  docling-serve:
    image: quay.io/docling-project/docling-serve:latest
    container_name: docling-serve
    ports:
      - "5001:5001"
    environment:
      - DOCLING_STORAGE_DIR=/data
      - DOCLING_PORT=5001
    volumes:
      - ./data/docling:/data
    restart: unless-stopped</code></pre>
<p>The configuration starts a <em>docling-serve</em> container from the <em>quay.io/docling-project/docling-serve</em> image. If the image isn&#8217;t available locally, Docker automatically pulls it from the registry. It also maps port <em>5001</em> on the host to port <em>5001</em> inside the container, allowing our application to communicate with the Docling server.</p>
<p>Finally, let&#8217;s start the Docling server by running the following command:</p>
<pre><code class="language-bash">$ docker compose up</code></pre>
<h3 id="bd-3-application-entry-point" data-id="3-application-entry-point">3.3. Application Entry Point</h3>
<div class="bd-anchor" id="3-application-entry-point"></div>
<p>Let&#8217;s create a <em>CamelDoclingApplication</em> class that serves as our application entry point:</p>
<pre><code class="language-java">public class CamelDoclingApplication {
    public static void main(String[] args) throws Exception {
        OpenAiChatModel model = OpenAiChatModel.builder()
          .baseUrl("http://langchain4j.dev/demo/openai/v1")
          .apiKey("demo")
          .modelName("gpt-4o-mini")
          .build();
        Main main = new Main();
        main.bind("chatModel", model);
        main.run(args);
    }
}</code></pre>
<p>We create an <em>OpenAiChatModel</em> object configured to use the <em>gpt-4o-mini</em> model. Although OpenAI models normally require a valid API key, LangChain4j provides a public demo endpoint and API key for experimentation. We configure the client to use this endpoint by setting the <em>baseUrl</em> to the LangChain4j demo service. Alternatively, we could configure LangChain4j to communicate with a locally hosted model through Ollama or replace the demo endpoint with our own OpenAI API credentials.</p>
<p>The <em>Main</em> instance provides a lightweight runtime for standalone Camel applications. We bind the chat model to Camel&#8217;s registry under the name <em>chatModel</em>, that Camel components and routes can reference during message processing.</p>
<h2 id="bd-converting-a-pdf-document-to-markdown" data-id="converting-a-pdf-document-to-markdown">4. Converting a PDF Document to Markdown</h2>
<div class="bd-anchor" id="converting-a-pdf-document-to-markdown"></div>
<p>Now that the Docling server is running, let&#8217;s create our first Camel route to convert <em>policies.pdf</em> into Markdown:</p>
<pre><code class="language-java">class ConversionRoute extends RouteBuilder {
    @Override
    public void configure() {
        from("file:documents?include=.*\\.(pdf|docx|pptx|html|md)&amp;noop=true&amp;idempotent=true")
          .routeId("document-analysis-workflow")
          .log("Processing document: ${header.CamelFileName}")
          .to("docling:CONVERT_TO_MARKDOWN?useDoclingServe=true&amp;doclingServeUrl=http://localhost:5001&amp;contentInBody=true")
          .setProperty("convertedMarkdown", body())
          .setHeader(Exchange.FILE_NAME, simple("${header.CamelFileName.replaceFirst('\\.[^.]+$', '')}.md"))
          .to("file:output") 
    }
}</code></pre>
<p>Here, we monitor the documents directory for supported document types. The <em>noop=true</em> option leaves the original file in place after processing, while <em>idempotent=true</em> prevents the same file from being processed more than once. Note that the Camel <em>file</em> component already provides the information that the Docling component needs to process the document. Although we could explicitly replace the message body with the document&#8217;s path, doing so would be redundant because the conversion works correctly without it.</p>
<p>Next, <strong>the <em>docling:CONVERT_TO_MARKDOWN </em>endpoint sends the document to the locally running <em>docling-serve</em> instance, which converts it into Markdown and returns the generated content in the message body</strong>.</p>
<p>Finally, we store the converted Markdown as an exchange property, rename the output file by replacing its original extension with <em>.md</em>, and use the Camel file component to write it in the output directory.</p>
<p><strong>If there are multiple documents in the <em>documents</em> directory, Camel processes each one independently and generates a corresponding Markdown file in the <em>output</em> directory.</strong></p>
<p>Let&#8217;s run our application by registering <em>ConversionRoute</em> with the Camel runtime:</p>
<pre><code class="language-java">Main main = new Main();
main.bind("chatModel", model);
main.configure()
  .addRoutesBuilder(new ConversionRoute());
main.run(args);</code></pre>
<p>Here, we register the <em>ConversionRoute</em> with Camel through the <em>Main</em> instance.</p>
<h2 id="bd-passing-the-markdown-document-to-langchain4j" data-id="passing-the-markdown-document-to-langchain4j">5. Passing the Markdown Document to LangChain4j</h2>
<div class="bd-anchor" id="passing-the-markdown-document-to-langchain4j"></div>
<p>Now that we&#8217;ve converted the document to Markdown, let&#8217;s send it to our chat model and ask it to analyze the document:</p>
<pre><code class="language-java">class ConversionRoute extends RouteBuilder {
    @Override
    public void configure() {
        from("file:documents?include=.*\\.(pdf|docx|pptx|html|md)&amp;noop=true&amp;idempotent=true")
          .routeId("document-analysis-workflow")
          // ...
          .setBody(simple("""
              You are a helpful document analysis assistant. Please analyze
              the following document and provide:
              1. A brief summary (2-3 sentences)
              2. Key topics and main points
              3. Any important findings or conclusions
              Document content:
              ${exchangeProperty.convertedMarkdown}
              """))
          .to("langchain4j-chat:analysis?chatModel=#chatModel")
          .setHeader(
              Exchange.FILE_NAME,
              simple("${header.CamelFileName.replaceFirst('\\.[^.]+$', '')}-analysis.md")
          )
          .to("file:analysis");
    }
}</code></pre>
<p>In this route, we construct a prompt that includes the Markdown generated by Docling and instruct the model to summarize and analyze the document. The <em>${exchangeProperty.convertedMarkdown}</em> expression inserts the Markdown stored earlier in the route into the prompt before it&#8217;s sent to the model.</p>
<p>Next, <strong>the <em>langchain4j-chat</em> component sends the prompt to the chat model referenced by <em>#chatModel</em></strong>. Camel resolves this reference from its registry, where we previously bound the <em>OpenAiChatModel</em> instance. The <em>analysis </em>in <em>langchain4j-chat:analysis </em>is the endpoint name. It&#8217;s primarily used to distinguish this endpoint from others.</p>
<p>Finally, we rename the output file by appending <em>-analysis</em> to the original filename and use the File component to write the model&#8217;s response to the <em>analysis</em> directory.</p>
<h2 id="bd-interactive-qampa-api" data-id="interactive-qampa-api">6. Interactive Q&amp;A API</h2>
<div class="bd-anchor" id="interactive-qampa-api"></div>
<p>Now, it&#8217;s time to define a question-answering API that uses the converted Markdown document as its knowledge source.</p>
<p>Let&#8217;s create a Camel route that accepts a question through an HTTP endpoint:</p>
<pre><code class="language-java">public class QuestionAndAnswerRoute extends RouteBuilder {
    @Override
    public void configure() throws Exception {
        from("undertow:http://0.0.0.0:8080/api/ask?httpMethodRestrict=POST").routeId("document-qa-api")
          .setProperty("question", bodyAs(String.class))
          .pollEnrich("file:output?fileName=policies.md&amp;noop=true&amp;idempotent=false")
          .setProperty("markdown", bodyAs(String.class))
          .setBody(simple("""
              You are a helpful document assistant.
              Answer ONLY using the document below.
              DOCUMENT
              =========
              ${exchangeProperty.markdown}
              QUESTION
              =========
              ${exchangeProperty.question}
              """))
          .to("langchain4j-chat:analysis?chatModel=#chatModel")
          .setHeader(Exchange.CONTENT_TYPE, constant("text/plain"));
    }
}
</code></pre>
<p>What happens here?</p>
<p>We expose the POST <em>/api/ask</em> endpoint through the embedded Undertow server. The request body contains the user&#8217;s question, which we store as an exchange property before replacing the message body later in the route.</p>
<p>The <em>pollEnrich</em> Enterprise Integration Pattern (EIP) reads the previously generated Markdown document from the <em>output</em> directory and places its contents into the current exchange. Rather than converting the PDF each time a request arrives, this route reads the Markdown file. This keeps the HTTP request lightweight by reusing the preprocessed document instead of invoking Docling for each question.</p>
<p>Also, we set <em>idempotent=false</em> so the File component can read the same Markdown file on every API request. By default, <em>noop=true</em> enables <em>idempotent=true</em>, which prevents the same file from being consumed more than once.</p>
<p>Then, we construct a prompt containing both the document and the user&#8217;s question before sending it to the <em>langchain4j-chat</em> component.</p>
<p>Finally, the model&#8217;s response is returned as the HTTP response body with a <em>text/plain</em> content type.</p>
<p>Let&#8217;s update our application entry point to register the <em>QuestionAndAnswerRoute</em>:</p>
<pre><code class="language-java">// ...
main.configure()
  .addRoutesBuilder(new QuestionAndAnswerRoute());
// ...</code></pre>
<p>Once the application is up,  we can run the following <em>curl</em> command to send a question to the API:</p>
<pre><code class="language-bash">curl -X POST \
  http://localhost:8080/api/ask \
  -H "Content-Type: text/plain" \
  -d "How many lifecycle stages does a task move through?"</code></pre>
<p>Here&#8217;s an API response we can expect:</p>
<pre><code class="language-bash">A task moves through four lifecycle stages: PENDING, IN_PROGRESS, REVIEW, and DONE.</code></pre>
<p>The API successfully answers the question based on the document we supplied.</p>
<h2 id="bd-conclusion" data-id="conclusion">7. Conclusion</h2>
<div class="bd-anchor" id="conclusion"></div>
<p>In this article, we learned how to integrate Apache Camel, Docling, and LangChain4j to build an intelligent document processing application. We used Docling to convert an unstructured document into a structured representation, orchestrated the conversion workflow with Apache Camel, and analyzed the converted content using an LLM through LangChain4j.</p>
<p>Additionally, we exposed a simple question-answer API that uses the converted document as context to generate grounded responses.</p>
<p>By converting documents into a structured representation once and reusing the generated Markdown for subsequent interactions, we simplify document processing workflows, avoid repeated conversions, and make it easier for LLMs to process the document content.</p>
<p>As always, the source code for the examples is available <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/eugenp/tutorials/tree/master/libraries-llms-2">over on GitHub</a>.</p><p>The post <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/apache-camel-docling-langchain4j">Building Intelligent Document Processing with Apache Camel, Docling and LangChain4j</a> first appeared on <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com">Baeldung</a>.</p><Img align="left" border="0" height="1" width="1" alt="" style="border:0;float:left;margin:0;padding:0;width:1px!important;height:1px!important;" hspace="0" src="https://feeds.feedblitz.com/~/i/964749371/0/baeldung">
<div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/964749371/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/964749371/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f07%2fJava-Featured-14-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/964749371/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/964749371/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/964749371/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/apache-camel-docling-langchain4j#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/apache-camel-docling-langchain4j/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</content:encoded>
					
					<wfw:commentRss>https://feeds.feedblitz.com/~/964749371/0/baeldung/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<webfeeds:featuredImage>https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-14-150x150.jpg</webfeeds:featuredImage></item>
<item>
<feedburner:origLink>https://www.baeldung.com/hibernate-common-table-expressions</feedburner:origLink>
		<title>CTE Support in Hibernate</title>
		<link>https://feeds.feedblitz.com/~/964749374/0/baeldung</link>
					<comments>https://feeds.feedblitz.com/~/964749374/0/baeldung#respond</comments>
		
		<dc:creator><![CDATA[Graham Cox]]></dc:creator>
		<pubDate>Thu, 30 Jul 2026 23:28:05 +0000</pubDate>
				<category><![CDATA[Persistence]]></category>
		<category><![CDATA[Hibernate]]></category>
		<category><![CDATA[HQL]]></category>
		<guid isPermaLink="false">https://www.baeldung.com/hibernate-common-table-expressions</guid>
					<description><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/11/Persistence-Featured-Image-02-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="max-width:100% !important;height:auto !important;float: left; margin-right: 5px;" loading="lazy" /><p>Learn how to use Common Table Expressions in Hibernate to help simplify writing complex queries by using intermediate named HQL result sets.</p>
<p>The post <a rel="NOFOLLOW" href="https://feeds.feedblitz.com/~/964749374/0/baeldung">CTE Support in Hibernate</a> first appeared on <a rel="NOFOLLOW" href="https://www.baeldung.com">Baeldung</a>.</p><div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/964749374/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/964749374/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f11%2fPersistence-Featured-Image-02-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/964749374/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/964749374/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/964749374/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/hibernate-common-table-expressions#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/hibernate-common-table-expressions/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</description>
										<content:encoded><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/11/Persistence-Featured-Image-02-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="float: left; margin-right: 5px;" decoding="async" loading="lazy" srcset="https://www.baeldung.com/wp-content/uploads/2024/11/Persistence-Featured-Image-02-1024x536.jpg 1024w, https://www.baeldung.com/wp-content/uploads/2024/11/Persistence-Featured-Image-02-300x157.jpg 300w, https://www.baeldung.com/wp-content/uploads/2024/11/Persistence-Featured-Image-02-768x402.jpg 768w, https://www.baeldung.com/wp-content/uploads/2024/11/Persistence-Featured-Image-02-100x52.jpg 100w, https://www.baeldung.com/wp-content/uploads/2024/11/Persistence-Featured-Image-02-600x314.jpg 600w, https://www.baeldung.com/wp-content/uploads/2024/11/Persistence-Featured-Image-02.jpg 1200w" sizes="auto, (max-width: 580px) 100vw, 580px" /><h2 id="bd-introduction" data-id="introduction"><strong>1. Introduction</strong></h2>
<div class="bd-anchor" id="introduction"></div>
<p>In this article, we’re going to look at how to use <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/sql/common-table-expressions">Common Table Expressions</a> (CTE) in Hibernate. We’ll see what a CTE is, what they&#8217;re used for, and how to use them in Hibernate queries.</p>
<p>Note that the introduction of CTE support is specific to Hibernate and HQL. This is not part of the standard JPA specification, and alternative implementations may not support CTE queries at all, or may support them in different ways.</p>
<h2 id="bd-what-are-common-table-expressions" data-id="what-are-common-table-expressions"><strong>2. What are Common Table Expressions</strong></h2>
<div class="bd-anchor" id="what-are-common-table-expressions"></div>
<p><strong>A Common Table Expression, or CTE, is a construct we can use within our queries to create a named result set that we can then refer to from within our main query.</strong></p>
<p>At their simplest, we can treat these as equivalent to subqueries or joins:</p>
<pre><code class="language-sql">-- Subquery
SELECT customer_id, total_spent
FROM (
    SELECT customer_id, SUM(amount) AS total_spent
    FROM orders
    GROUP BY customer_id
) customer_totals
WHERE total_spent &gt; (
    SELECT AVG(total_spent)
    FROM (
        SELECT customer_id, SUM(amount) AS total_spent
        FROM orders
        GROUP BY customer_id
    ) customer_totals
);
-- CTE
WITH customer_totals AS (
    SELECT customer_id, SUM(amount) AS total_spent
    FROM orders
    GROUP BY customer_id
)
SELECT customer_id, total_spent
FROM customer_totals
WHERE total_spent &gt; ( SELECT AVG(total_spent) FROM customer_totals );</code></pre>
<p>These two SQL statements do the same thing. They each return the total spend of all customers who have spent more than the average amount. However, the CTE version works by defining a name, <em>customer_totals, </em>for our subquery and then allowing us to reference that name whenever we want. This is both easier to understand and more efficient for the database to execute.</p>
<h2 id="bd-setting-up" data-id="setting-up"><strong>3. Setting Up</strong></h2>
<div class="bd-anchor" id="setting-up"></div>
<p>Now that we know what CTEs are, we&#8217;re ready to start using them. Before we can do this, we need to do a little bit of setting up.</p>
<h3 id="bd-1-dependencies" data-id="1-dependencies"><strong>3.1. Dependencies</strong></h3>
<div class="bd-anchor" id="1-dependencies"></div>
<p><strong>To use CTEs in <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-core">Hibernate</a>, we need to use version 6.2 or newer. The newest release at the time of writing is <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-core/7.4.4.Final">7.4.4.Final</a>:</strong></p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.hibernate.orm&lt;/groupId&gt;
    &lt;artifactId&gt;hibernate-core&lt;/artifactId&gt;
    &lt;version&gt;7.4.4.Final&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p>If we&#8217;re getting Hibernate via Spring Boot, this means Spring Boot version 3.1 or newer is needed:</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;
&lt;/dependency&gt;</code></pre>
<p>Now we have our dependencies, let&#8217;s next consider what data we&#8217;ll be querying.</p>
<h3 id="bd-2-database" data-id="2-database"><strong>3.2. Database</strong></h3>
<div class="bd-anchor" id="2-database"></div>
<p><strong>For this article, we need to set up a database.</strong> Our table will look as follows:</p>
<pre><code class="language-sql">CREATE TABLE employee (
    id         BIGINT PRIMARY KEY,
    name       VARCHAR(100)   NOT NULL,
    title      VARCHAR(100)   NOT NULL,
    department VARCHAR(50)    NOT NULL,
    salary     NUMERIC(10, 2) NOT NULL,
    manager_id BIGINT REFERENCES employee (id)
);
</code></pre>
<p>Note that the <em>manager_id </em>column references back to the same <em>employee </em>table. This allows us to store a hierarchical structure in a single table.</p>
<p>We then need some data in our database:</p>
<table style="height: 312px;width: 100%;border-collapse: collapse">
<tbody>
<tr style="height: 24px">
<td style="width: 16.6667%;height: 24px;border-style: none"><strong>ID</strong></td>
<td style="width: 16.4847%;height: 24px;border-style: none"><strong>Name</strong></td>
<td style="width: 26.2009%;height: 24px;border-style: none"><strong>Title</strong></td>
<td style="width: 14.9563%;height: 24px;border-style: none"><strong>Department</strong></td>
<td style="width: 10.0437%;height: 24px;border-style: none"><strong>Salary</strong></td>
<td style="width: 15.5022%;height: 24px;border-style: none"><strong>Manager</strong></td>
</tr>
<tr style="height: 24px">
<td style="width: 16.6667%;height: 24px;border-style: none"><strong>1</strong></td>
<td style="width: 16.4847%;height: 24px;border-style: none">Alice Turner</td>
<td style="width: 26.2009%;height: 24px;border-style: none">CEO</td>
<td style="width: 14.9563%;height: 24px;border-style: none">Executive</td>
<td style="width: 10.0437%;height: 24px;border-style: none">220,000</td>
<td style="width: 15.5022%;height: 24px;border-style: none">—</td>
</tr>
<tr style="height: 24px">
<td style="width: 16.6667%;height: 24px;border-style: none"><strong>2</strong></td>
<td style="width: 16.4847%;height: 24px;border-style: none">Ben Rodgers</td>
<td style="width: 26.2009%;height: 24px;border-style: none">VP Engineering</td>
<td style="width: 14.9563%;height: 24px;border-style: none">Engineering</td>
<td style="width: 10.0437%;height: 24px;border-style: none">180,000</td>
<td style="width: 15.5022%;height: 24px;border-style: none">Alice Turner (1)</td>
</tr>
<tr style="height: 24px">
<td style="width: 16.6667%;height: 24px;border-style: none"><strong>3</strong></td>
<td style="width: 16.4847%;height: 24px;border-style: none">Carla Nunes</td>
<td style="width: 26.2009%;height: 24px;border-style: none">VP Sales</td>
<td style="width: 14.9563%;height: 24px;border-style: none">Sales</td>
<td style="width: 10.0437%;height: 24px;border-style: none">175,000</td>
<td style="width: 15.5022%;height: 24px;border-style: none">Alice Turner (1)</td>
</tr>
<tr style="height: 24px">
<td style="width: 16.6667%;height: 24px;border-style: none"><strong>4</strong></td>
<td style="width: 16.4847%;height: 24px;border-style: none">David Kim</td>
<td style="width: 26.2009%;height: 24px;border-style: none">Engineering Manager</td>
<td style="width: 14.9563%;height: 24px;border-style: none">Engineering</td>
<td style="width: 10.0437%;height: 24px;border-style: none">140,000</td>
<td style="width: 15.5022%;height: 24px;border-style: none">Ben Rodgers (2)</td>
</tr>
<tr style="height: 24px">
<td style="width: 16.6667%;height: 24px;border-style: none"><strong>5</strong></td>
<td style="width: 16.4847%;height: 24px;border-style: none">Ella Novak</td>
<td style="width: 26.2009%;height: 24px;border-style: none">Engineering Manager</td>
<td style="width: 14.9563%;height: 24px;border-style: none">Engineering</td>
<td style="width: 10.0437%;height: 24px;border-style: none">138,000</td>
<td style="width: 15.5022%;height: 24px;border-style: none">Ben Rodgers (2)</td>
</tr>
<tr style="height: 24px">
<td style="width: 16.6667%;height: 24px;border-style: none"><strong>6</strong></td>
<td style="width: 16.4847%;height: 24px;border-style: none">Frank Osei</td>
<td style="width: 26.2009%;height: 24px;border-style: none">Sales Manager</td>
<td style="width: 14.9563%;height: 24px;border-style: none">Sales</td>
<td style="width: 10.0437%;height: 24px;border-style: none">120,000</td>
<td style="width: 15.5022%;height: 24px;border-style: none">Carla Nunes (3)</td>
</tr>
<tr style="height: 24px">
<td style="width: 16.6667%;height: 24px;border-style: none"><strong>7</strong></td>
<td style="width: 16.4847%;height: 24px;border-style: none">Grace Lin</td>
<td style="width: 26.2009%;height: 24px;border-style: none">Senior Engineer</td>
<td style="width: 14.9563%;height: 24px;border-style: none">Engineering</td>
<td style="width: 10.0437%;height: 24px;border-style: none">115,000</td>
<td style="width: 15.5022%;height: 24px;border-style: none">David Kim (4)</td>
</tr>
<tr style="height: 24px">
<td style="width: 16.6667%;height: 24px;border-style: none"><strong>8</strong></td>
<td style="width: 16.4847%;height: 24px;border-style: none">Hugo Alvarez</td>
<td style="width: 26.2009%;height: 24px;border-style: none">Software Engineer</td>
<td style="width: 14.9563%;height: 24px;border-style: none">Engineering</td>
<td style="width: 10.0437%;height: 24px;border-style: none">95,000</td>
<td style="width: 15.5022%;height: 24px;border-style: none">David Kim (4)</td>
</tr>
<tr style="height: 24px">
<td style="width: 16.6667%;height: 24px;border-style: none"><strong>9</strong></td>
<td style="width: 16.4847%;height: 24px;border-style: none">Ivy Chen</td>
<td style="width: 26.2009%;height: 24px;border-style: none">Software Engineer</td>
<td style="width: 14.9563%;height: 24px;border-style: none">Engineering</td>
<td style="width: 10.0437%;height: 24px;border-style: none">98,000</td>
<td style="width: 15.5022%;height: 24px;border-style: none">Ella Novak (5)</td>
</tr>
<tr style="height: 24px">
<td style="width: 16.6667%;height: 24px;border-style: none"><strong>10</strong></td>
<td style="width: 16.4847%;height: 24px;border-style: none">Jack Meyer</td>
<td style="width: 26.2009%;height: 24px;border-style: none">Sales Rep</td>
<td style="width: 14.9563%;height: 24px;border-style: none">Sales</td>
<td style="width: 10.0437%;height: 24px;border-style: none">80,000</td>
<td style="width: 15.5022%;height: 24px;border-style: none">Frank Osei (6)</td>
</tr>
<tr style="height: 24px">
<td style="width: 16.6667%;height: 24px;border-style: none"><strong>11</strong></td>
<td style="width: 16.4847%;height: 24px;border-style: none">Kara Diaz</td>
<td style="width: 26.2009%;height: 24px;border-style: none">Sales Rep</td>
<td style="width: 14.9563%;height: 24px;border-style: none">Sales</td>
<td style="width: 10.0437%;height: 24px;border-style: none">82,000</td>
<td style="width: 15.5022%;height: 24px;border-style: none">Frank Osei (6)</td>
</tr>
<tr style="height: 24px">
<td style="width: 16.6667%;height: 24px;border-style: none"><strong>12</strong></td>
<td style="width: 16.4847%;height: 24px;border-style: none">Liam Foster</td>
<td style="width: 26.2009%;height: 24px;border-style: none">Junior Engineer</td>
<td style="width: 14.9563%;height: 24px;border-style: none">Engineering</td>
<td style="width: 10.0437%;height: 24px;border-style: none">75,000</td>
<td style="width: 15.5022%;height: 24px;border-style: none">Grace Lin (7)</td>
</tr>
</tbody>
</table>
<p>This gives us a set of employees that make a tree structure, rooted at &#8220;Alice&#8221;.</p>
<h3 id="bd-2-jpa-entity" data-id="2-jpa-entity"><strong>3.2. JPA Entity</strong></h3>
<div class="bd-anchor" id="2-jpa-entity"></div>
<p><strong>Finally, since we&#8217;re going to be working with Hibernate, we also need a Hibernate entity to represent our data:</strong></p>
<pre><code class="language-java">@Entity
@Table(name = "employee")
public class Employee {
    @Id
    private Long id;
    private String name;
    private String title;
    private String department;
    private BigDecimal salary;
    @ManyToOne
    @JoinColumn(name = "manager_id")
    private Employee manager;
}</code></pre>
<p>The <em>Employee</em> entity gives us access to all of our data. <strong>As with our table structure, we&#8217;ve also got our reference back to the same entity.</strong> This time it&#8217;s in the form of a <em>manager </em>field that gives us the complete row.</p>
<h2 id="bd-basic-ctes-in-hibernate" data-id="basic-ctes-in-hibernate"><strong>4. Basic CTEs in Hibernate</strong></h2>
<div class="bd-anchor" id="basic-ctes-in-hibernate"></div>
<p>Now that we&#8217;ve got our database and entity, we&#8217;re ready to query it.</p>
<p><strong>As with SQL, Hibernate allows us to use the <em>WITH </em>clause in our HQL queries. This is used to define a new named result set that we can treat as any table or view within our main query.</strong></p>
<p>For example:</p>
<pre><code class="language-java">List&lt;Employee&gt; employees = session.createQuery("""
    WITH engineers AS (
        SELECT e.id AS id
        FROM Employee e
        WHERE e.department = 'Engineering'
    )
    SELECT e
    FROM Employee e
    WHERE e.id IN (SELECT id FROM engineers)
    AND e.salary &gt; 100000
    ORDER BY e.name ASC
""", Employee.class).getResultList();
</code></pre>
<p>We can consider this in two parts. Our <em>WITH </em>clause generates a new result set named <em>engineers </em>consisting of all of the IDs of employees in the &#8220;Engineering&#8221; department. Our main query then uses this in a sub-select to only return the <em>Employee </em>records that match.</p>
<p><strong>Note that the return type from our <em>WITH </em>clause is always an <em>AnonymousTupleType</em>.</strong>This means that we can only include individual values in it, and not entire entities. We also need to give every value a distinct name with <em>AS</em>. We can then refer to these values by name in the main query.</p>
<p>Here, the <em>engineers </em>result set contains IDs 2, 4, 5, 7, 8, 9, and 12. Our <em>IN </em>clause then matches only those when selecting <em>Employee </em>entities, before further filtering by salary, to give an overall result of &#8220;Ben&#8221;, &#8220;David&#8221;, &#8220;Ella&#8221; and &#8220;Grace&#8221;.</p>
<p>We can also make our <em>WITH </em>clause more complicated, allowing us to resolve this complexity once and refer to it as needed in our main query:</p>
<pre><code class="language-java">List&lt;Employee&gt; employees = session.createQuery("""
    WITH dept_avg AS (
        SELECT e.department AS department, AVG(e.salary) AS avgSalary
        FROM Employee e
        GROUP BY e.department
    )
    SELECT e
    FROM Employee e
    JOIN dept_avg d ON e.department = d.department
    WHERE e.salary &gt; d.avgSalary
    ORDER BY e.department, e.salary DESC
""", Employee.class).getResultList();
</code></pre>
<p>This time, our <em>WITH </em>clause is an aggregate query that will calculate the average salary for each department. On a very large table, this could be expensive. Writing it as a CTE separates the calculation from the main query, making the query easier to read and maintain. <strong>Depending on the database and its optimizer, the CTE may be materialized once or inlined into the main query, whichever produces the most efficient execution plan.</strong></p>
<p>Here, our <em>dept_avg </em>result set will be as follows:</p>
<table style="border-collapse: collapse;width: 100%">
<tbody>
<tr>
<td style="width: 50%"><strong>department</strong></td>
<td style="width: 50%"><strong>avgSalary</strong></td>
</tr>
<tr>
<td style="width: 50%">CEO</td>
<td style="width: 50%">220,000</td>
</tr>
<tr>
<td style="width: 50%">Engineering</td>
<td style="width: 50%">120,142.85714286</td>
</tr>
<tr>
<td style="width: 50%">Sales</td>
<td style="width: 50%">114,250</td>
</tr>
</tbody>
</table>
<p>Our main query can then join to this by name, as if it were a table or view in the database. This then allows us to combine the data from our <em>dept_avg </em>result set with the data from our main <em>Employee </em>entity to match the rows that we&#8217;re interested in. In this case, &#8220;Ben&#8221;, &#8220;David&#8221;, &#8220;Ella&#8221;, &#8220;Carla&#8221; and &#8220;Frank&#8221;.</p>
<h2 id="bd-recursive-ctes" data-id="recursive-ctes"><strong>5. Recursive CTEs</strong></h2>
<div class="bd-anchor" id="recursive-ctes"></div>
<p><strong>The place that CTEs can become really powerful is when we want to do any graph-based queries. We can write our <em>WITH </em>clause to be recursive.</strong> We do this by splitting it into two parts with a <em>UNION</em>, and can then reference back to the same named <em>WITH </em>clause from within this second part:</p>
<p>For example, let&#8217;s start with a simple CTE query:</p>
<pre><code class="language-java">List&lt;Employee&gt; employees = session.createQuery("""
    WITH subordinates AS (
        SELECT e.id AS id FROM Employee e WHERE e.manager.id = 2
    )
    SELECT e
    FROM Employee e
    WHERE e.id IN (SELECT s.id FROM subordinates s)
    ORDER BY e.department, e.name
    """, Employee.class).getResultList();
</code></pre>
<p>On its own, this is an overly complicated way to return every <em>Employee </em>who has a manager ID of &#8220;2&#8221;. Running this as-is will return &#8220;David&#8221; and &#8220;Ella&#8221;.</p>
<p>However, we can extend the <em>WITH </em>clause to be recursive as follows:</p>
<pre><code class="language-java">List&lt;Employee&gt; employees = session.createQuery("""
    WITH subordinates AS (
        SELECT e.id AS id FROM Employee e WHERE e.manager.id = 2
    
        UNION
    
        SELECT e2.id AS id FROM Employee e2 JOIN subordinates s ON e2.manager.id = s.id
    )
    SELECT e
    FROM Employee e
    WHERE e.id IN (SELECT s.id FROM subordinates s)
    ORDER BY e.department, e.name
    """, Employee.class).getResultList();
</code></pre>
<p>All we&#8217;ve added here is the <em>UNION </em>and the second part of our <em>WITH </em>clause. <strong>This now selects every <em>Employee </em>whose manager ID is either &#8220;2&#8221; or is part of the already built list of IDs.</strong> This will then return &#8220;David&#8221;, &#8220;Ella&#8221;, &#8220;Grace&#8221;, &#8220;Hugo&#8221;, &#8220;Ivy&#8221; and &#8220;Liam&#8221;.</p>
<a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/wp-content/uploads/2026/07/cte-employees.png"><img loading="lazy" decoding="async" class="alignnone wp-image-255575 size-large" src="https://www.baeldung.com/wp-content/uploads/2026/07/cte-employees-1024x783.png" alt="A hierarchical tree diagram showing nine numbered people connected by parent-child relationships. At the top is Alice (1). Alice branches to Ben (2) on the left and Carla (3) on the right. Ben has two children: David (4) and Ella (5). David has two children, Grace (7) and Hugo (8). Grace has one child, Liam (12). Ella has one child, Ivy (9). Carla has one child, Frank (6). The nodes for David (4), Ella (5), Grace (7), Hugo (8), Ivy (9), and Liam (12) are shaded light green, while Alice (1), Ben (2), Carla (3), and Frank (6) have white backgrounds. Arrows point from each parent to their children." width="580" height="443" /></a>
<p>This query even reached as far as &#8220;Liam&#8221;, who is 3 steps away from our original ID, and it would continue as far as the data allowed it.</p>
<p>Note that by using <em>UNION </em>instead of <em>UNION ALL</em>, if we ever encounter a situation where the data is cyclical, we&#8217;ll only return each unique row once.</p>
<h2 id="bd-summary" data-id="summary"><strong>6. Summary</strong></h2>
<div class="bd-anchor" id="summary"></div>
<p>In this article, we&#8217;ve had a very brief look at using CTEs in Hibernate. We&#8217;ve seen what they are, how they work, and how we can use them. Next time you need queries like this, why not give it a go?</p>
<p>As always, all the code from this article is available over on <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/eugenp/tutorials/tree/master/persistence-modules/hibernate-queries-2">GitHub</a>.</p><p>The post <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/hibernate-common-table-expressions">CTE Support in Hibernate</a> first appeared on <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com">Baeldung</a>.</p><Img align="left" border="0" height="1" width="1" alt="" style="border:0;float:left;margin:0;padding:0;width:1px!important;height:1px!important;" hspace="0" src="https://feeds.feedblitz.com/~/i/964749374/0/baeldung">
<div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/964749374/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/964749374/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f11%2fPersistence-Featured-Image-02-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/964749374/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/964749374/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/964749374/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/hibernate-common-table-expressions#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/hibernate-common-table-expressions/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</content:encoded>
					
					<wfw:commentRss>https://feeds.feedblitz.com/~/964749374/0/baeldung/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<webfeeds:featuredImage>https://www.baeldung.com/wp-content/uploads/2024/11/Persistence-Featured-Image-02-150x150.jpg</webfeeds:featuredImage></item>
<item>
<feedburner:origLink>https://www.baeldung.com/llm-integration-apache-camel-openai-component</feedburner:origLink>
		<title>LLM Integration With Apache Camel OpenAI Component</title>
		<link>https://feeds.feedblitz.com/~/964742375/0/baeldung</link>
					<comments>https://feeds.feedblitz.com/~/964742375/0/baeldung#respond</comments>
		
		<dc:creator><![CDATA[Burak Gökmen]]></dc:creator>
		<pubDate>Thu, 30 Jul 2026 23:25:13 +0000</pubDate>
				<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[OpenAI]]></category>
		<guid isPermaLink="false">https://www.baeldung.com/?p=204320</guid>
					<description><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="max-width:100% !important;height:auto !important;float: left; margin-right: 5px;" loading="lazy" /><p>Learn about LLM integration with the Apache Camel OpenAI component with examples.</p>
<p>The post <a rel="NOFOLLOW" href="https://feeds.feedblitz.com/~/964742375/0/baeldung">LLM Integration With Apache Camel OpenAI Component</a> first appeared on <a rel="NOFOLLOW" href="https://www.baeldung.com">Baeldung</a>.</p><div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/964742375/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/964742375/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f07%2fJava-Featured-12-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/964742375/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/964742375/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/964742375/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/llm-integration-apache-camel-openai-component#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/llm-integration-apache-camel-openai-component/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</description>
										<content:encoded><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="float: left; margin-right: 5px;" decoding="async" loading="lazy" srcset="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-1024x536.jpg 1024w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-300x157.jpg 300w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-768x402.jpg 768w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-100x52.jpg 100w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12.jpg 1200w" sizes="auto, (max-width: 580px) 100vw, 580px" /><h2 id="bd-overview" data-id="overview">1. Overview</h2>
<div class="bd-anchor" id="overview"></div>
<p><a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://camel.apache.org/components/4.18.x/openai-component.html">The Apache Camel OpenAI component</a>, introduced in Camel 4.17, enables integration of Java applications with OpenAI or OpenAI-compatible LLM (Large Language Model) servers such as Ollama or vLLM. Basically, it uses Camel routes for interaction with LLMs and handles message conversion between Camel Exchanges and OpenAI requests. It uses <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/java-openai-api-client">the OpenAI Java API library</a> under the hood.</p>
<p>In this tutorial, we’ll discuss LLM integration with the Apache Camel OpenAI component and see several examples.</p>
<h2 id="bd-maven-dependencies" data-id="maven-dependencies">2. Maven Dependencies</h2>
<div class="bd-anchor" id="maven-dependencies"></div>
<p>We have to add the following <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.apache.camel/camel-openai">Maven dependency</a> to our <em>pom.xml</em>:</p>
<pre><code class="language-plaintext">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.camel&lt;/groupId&gt;
    &lt;artifactId&gt;camel-openai&lt;/artifactId&gt;
    &lt;version&gt;4.18.0&lt;/version&gt;
&lt;/dependency&gt;</code></pre>
<h2 id="bd-basic-interaction-with-an-llm" data-id="basic-interaction-with-an-llm">3. Basic Interaction With an LLM</h2>
<div class="bd-anchor" id="basic-interaction-with-an-llm"></div>
<p><strong>The component uses the standard Camel URI (Uniform Resource Identifier) format: <em>openai:operation[?options]</em></strong>. The <em>openai</em> part of the URI is fixed.</p>
<p>The <em>operation</em> part corresponds to the URI’s path parameter. <strong>Two operations are supported: <em>chat-completion</em> and <em>embeddings</em></strong>. <em>chat-completion</em>, which is of interest to us, generates chat responses using a model. The other operation, <em>embeddings</em>, is useful for creating vector embeddings, for example, in <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/cs/retrieval-augmented-generation">RAG (Retrieval-Augmented Generation)</a> pipelines.</p>
<p>The <em>options</em> part corresponds to the URI’s query parameters. These are generally configuration options such as model choices, generation settings, and authentication parameters. For example, the <em>model</em> option specifies which LLM to use. We can also pass these options using message headers instead of the URI.</p>
<h3 id="bd-1-chat-application" data-id="1-chat-application">3.1. Chat Application</h3>
<div class="bd-anchor" id="1-chat-application"></div>
<p>In this section, we’ll examine a simple chat application that interacts with an LLM. The application sends a prompt to the model and displays the model’s response.</p>
<p>Let&#8217;s start by using the Apache Camel’s <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://camel.apache.org/components/4.18.x/others/main.html"><em>Main</em></a> class to configure, run, and handle the lifecycle of a standalone Apache Camel application:</p>
<pre><code class="language-java">Main main = new Main();
main.configure()
  .addRoutesBuilder(new ChatRoute());
main.start();
</code></pre>
<p>The <em>addRoutesBuilder()</em> method registers a custom routing class, <em>ChatRoute</em>, that implements the routing rules. <strong>The routing logic includes how Apache Camel takes a prompt, wraps it in an HTTP call to an LLM, and processes the response</strong>. The <em>start()</em> method of the <em>Main</em> class starts the runtime, instantiates the routes, and begins processing prompts.</p>
<p>Then, let&#8217;s send a message to an endpoint using the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://camel.apache.org/manual/producertemplate.html"><em>ProducerTemplate</em></a> class:</p>
<pre><code class="language-java">ProducerTemplate template = main.getCamelContext()
    .createProducerTemplate();
String aiResponse = template.requestBody("direct:startChat", userPrompt, String.class);
</code></pre>
<p>We use the <em>requestBody()</em> method of <em>ProducerTemplate</em> to pass the prompt to the route. The first argument, <em>&#8220;direct:startChat&#8221;</em>, is the endpoint that the LLM exposes. <strong><em>direct:</em> is an Apache Camel component providing synchronous invocation of routes</strong>. <em>startChat</em> is the name we give to the endpoint.</p>
<p>Our application uses this endpoint to communicate with the LLM. We pass the second argument, <em>userPrompt</em>, to the LLM and store its response in the variable <em>aiResponse</em>. The third argument, <em>String.class</em>, tells Apache Camel that we expect the response to be of type <em>String</em>. Therefore, the type of <em>aiResponse</em> is <em>String</em>.</p>
<h3 id="bd-2-the-routing-class" data-id="2-the-routing-class">3.2. The Routing Class</h3>
<div class="bd-anchor" id="2-the-routing-class"></div>
<p>Next, let’s discuss the routing class. <strong>We extend the <em>ChatRoute</em> class from the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://camel.apache.org/manual/route-builder.html"><em>RouteBuilder</em></a> class to create routing rules using the Java <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-integration-java-dsl">DSL (Domain-Specific Language)</a></strong>:</p>
<pre><code class="language-java">public class ChatRoute extends RouteBuilder {
    @Override
    public void configure() throws Exception {
        onException(Exception.class).handled(true)
          .log(org.apache.camel.LoggingLevel.INFO, "OpenAI Error: ${exception.message}")
          .setBody(simple("OpenAI request failed."));
        from("direct:startChat")
          .setHeader(
            OpenAIConstants.SYSTEM_MESSAGE,
            constant("You are a helpful, brief IT support assistant."))
          .setHeader(OpenAIConstants.TEMPERATURE, constant(0.1))
          .setHeader(OpenAIConstants.MODEL, constant("gpt-4o-mini"))
          .to("openai:chat-completion")
          .log("AI Response: ${body}");
    }
}
</code></pre>
<p>We override the <em>configure()</em> method, which acts as the entry point. <strong><em>from(&#8220;direct:startChat&#8221;)</em> defines a synchronous endpoint named <em>startChat</em></strong>. As we saw in the previous subsection, applications can send user prompts to this endpoint by calling <em>template.requestBody(&#8220;direct:startChat&#8221;, userPrompt, String.class)</em>.</p>
<p>The three <em>setHeader()</em> calls attach OpenAI-specific message headers by injecting the following key-value pairs:</p>
<ul>
<li><em>SYSTEM_MESSAGE</em> sets the system prompt to <em>&#8220;You are a helpful, brief IT support assistant.&#8221;</em> and therefore defines the behavior and tone of the model.</li>
<li><em>MODEL</em> picks <em>gpt-4o-mini</em> as the target LLM.</li>
<li><em>TEMPERATURE</em> sets the sampling temperature to <em>1</em>, thereby making the model more deterministic.</li>
</ul>
<p><strong><em>to(&#8220;openai:chat-completion&#8221;)</em></strong><strong> is the actual call to the OpenAI component</strong>. It extracts the message body (i.e., the user prompt) and the headers, performs the OpenAI chat completion request, and waits for the response.</p>
<p>Instead of using message headers above, we can also use the Camel URI. For example, we can set the <em>model</em> and <em>temperature</em> options using the URI:</p>
<pre><code class="language-java">.to("openai:chat-completion?model=gpt-4o-mini&amp;temperature=0.1")</code></pre>
<p>Finally,<em> log(&#8220;AI Response: ${body}&#8221;)</em> prints the model’s response.</p>
<p><strong>We must authenticate our application while making requests to OpenAI’s developer platform</strong>. One method for authentication is to set the confidential OpenAI API key to the <em>OPENAI_API_KEY</em> environment variable from the terminal we’ll run the application in Linux:</p>
<pre><code class="language-bash">$ export OPENAI_API_KEY=&lt;our_confidential_actual_key_here&gt;</code></pre>
<p>Let&#8217;s see the response of the LLM when we send the prompt, <em>&#8220;Give me information about Baeldung&#8221;</em>:</p>
<pre><code class="language-plaintext">AI Response: Baeldung is a popular online platform that provides tutorials and articles primarily focused on Java, 
Spring Framework, and related technologies.
...
The content is well-structured and often includes practical examples, 
making it a valuable resource for both beginners and experienced developers.</code></pre>
<p>The output shows that we received a successful response to our prompt. We abbreviated the response as it’s long.</p>
<h2 id="bd-using-other-openai-compatible-endpoints" data-id="using-other-openai-compatible-endpoints">4. Using Other OpenAI-Compatible Endpoints</h2>
<div class="bd-anchor" id="using-other-openai-compatible-endpoints"></div>
<p><strong>The Apache Camel OpenAI component allows us to switch easily to other OpenAI-compatible endpoints using the <em>baseUrl</em> option</strong>. The endpoint can point to local or third-party providers. For example, we just need to change the router code as follows to use a local Ollama server:</p>
<pre><code class="language-java">.setHeader(OpenAIConstants.MODEL, constant("qwen3.5:9b"))
.to("openai:chat-completion?baseUrl=http://localhost:11434/v1")
</code></pre>
<p><strong>The default value of <em>baseUrl</em> is </strong><strong><em>https://api.openai.com/v1</em></strong>. We used the default OpenAI endpoint in the previous section. Therefore, we didn’t specify it explicitly. However, by setting <em>baseUrl</em> to <em>http://localhost:11434/v1</em>, we send the requests to the local Ollama server instead of OpenAI’s servers.</p>
<p>We also set the model to <em>qwen3.5:9b</em> in our example. Before sending prompts to the model, the model must have been run locally using the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/linux/genai-ollama-installation#4-installing-and-testing-a-large-language-model"><em>ollama run</em></a> command. We don’t need to use a real API key while using Ollama locally. But an API key is still required. So we can set it to a dummy value for compatibility with OpenAI. But third-party providers over the cloud, like Ollama’s cloud service, require a real API key.</p>
<h2 id="bd-keeping-conversational-memory" data-id="keeping-conversational-memory">5. Keeping Conversational Memory</h2>
<div class="bd-anchor" id="keeping-conversational-memory"></div>
<p><strong>By default, conversational memory isn’t retained by the OpenAI component; i.e., OpenAI doesn’t remember the responses to previous prompts</strong>. For example, if we tell an LLM our name in one prompt and then ask for it in the next prompt, it will usually respond that it doesn&#8217;t know our name.</p>
<p>To maintain conversational memory with the OpenAI component, we need to make two changes to the previous example. Firstly, <strong>we must reuse the same <em>Exchange</em> object on the application side</strong>:</p>
<pre><code class="language-java">Exchange exchange = template.getCamelContext()
  .getEndpoint("direct:startChatWithMemory")
  .createExchange();
for (String prompt : prompts) {
    exchange.getMessage()
      .setBody(prompt);
    template.send("direct:startChatWithMemory", exchange);
                
aiResponses.add(exchange.getMessage()
  .getBody(String.class));
</code></pre>
<p>Once the <em>Exchange</em> object is created, namely <em>exchange</em> in the code snippet above, we use the same object in a <em>for</em> loop for processing the prompts. The <em>prompts</em> variable is a list of strings containing the prompts:</p>
<pre><code class="language-java">List&lt;String&gt; prompts = List.of("My name is Burak and I am a Java developer.", "What is my name and what do I do?"
);
</code></pre>
<p>The first prompt specifies the user’s name and job, and the second prompt asks them.</p>
<p><strong>Secondly, we must set the <em>conversationMemory</em> option to <em>true</em> on the router side</strong>:</p>
<pre><code class="language-java">.to("openai:chat-completion?conversationMemory=true")</code></pre>
<p>When we run the application, the response to the first prompt, <em>&#8220;My name is Burak and I am a Java developer.&#8221;</em>, is as follows:</p>
<pre><code class="language-plaintext">Hi Burak! How can I assist you today with your Java development?</code></pre>
<p>Let&#8217;s see the response to the second prompt, <em>&#8220;What is my name and what do I do?&#8221;</em>:</p>
<pre><code class="language-plaintext">Your name is Burak, and you are involved in Java development. How can I assist you further?</code></pre>
<p>Therefore, reusing the same <em>Exchange</em> object while setting the <em>conversationMemory</em> option to <em>true</em> enables us to persist storage between calls.</p>
<h2 id="bd-providing-structured-output" data-id="providing-structured-output">6. Providing Structured Output</h2>
<div class="bd-anchor" id="providing-structured-output"></div>
<p><strong>The OpenAI component can also present the LLM’s response in a deterministic format</strong>. For example, we can map the response to a <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/java-pojo-class">POJO (Plain Old Java Object)</a>, which can be directed safely to a database. Let’s discuss an example that returns a book’s name, author, and summary:</p>
<pre><code class="language-java">public static BookInfo runChat(String userPrompt) throws Exception {
    ...
    BookInfo bookInfo = template.requestBody("direct:startBookChat", userPrompt, BookInfo.class);
    return bookInfo;
    ...
}
</code></pre>
<p>The <em>runChat()</em> member function in the above code snippet takes a user prompt as a string and passes it to the router by calling the <em>requestBody()</em> method of <em>ProducerTemplate</em>. <strong>The third argument of <em>requestBody()</em>, <em>BookInfo.class</em>, specifies the response to be a <em>BookInfo</em> object</strong>. The <em>BookInfo</em> class consists of three public member variables:</p>
<pre><code class="language-java">public class BookInfo {
    public String name;
    public String author;
    public String summary;
}</code></pre>
<p>We also need to update the router class:</p>
<pre><code class="language-java">from("direct:startBookChat")
  .setHeader(OpenAIConstants.SYSTEM_MESSAGE, constant("You are a helpful, brief book advisor."))
  .setHeader(OpenAIConstants.MODEL, constant("gpt-4o-mini"))
  .setHeader(OpenAIConstants.TEMPERATURE, constant(0.1))
  .to("openai:chat-completion?outputClass=com.baeldung.apachecamel.BookInfo")
  .unmarshal()
  .json(JsonLibrary.Jackson, BookInfo.class)
  .log("AI Response: ${body}");</code></pre>
<p>Here, <strong>we use the <em>outputClass</em> option to instruct the LLM to respond in JSON format that matches the <em>BookInfo</em> schema</strong>. The next calls, <em>unmarhal()</em> and<em> json(JsonLibrary.Jackson, BookInfo.class)</em>, use the Apache Camel JSON Jackson component. They convert the raw JSON string in the message body and deserialize it to <em>BookInfo</em>.</p>
<p>Let&#8217;s see an example of asking for the author and summary of a book, <em>The Time Regulation Institute</em>:</p>
<pre><code class="language-java">String userPrompt = "Can you provide the author and summary of the following book: The Time Regulation Institute";
BookInfo book = runChat(userPrompt);
LOGGER.info("Book: {}", book.name);
LOGGER.info("Author: {}", book.author);
LOGGER.info("Summary: {}", book.summary);</code></pre>
<p>Here is the abbreviated output:</p>
<pre><code class="language-plaintext">Book: The Time Regulation Institute
Author: Ahmet Hamdi Tanpınar
Summary: A satirical novel that explores the absurdities of modernity and the clash between 
tradition and progress in Turkish society, focusing on the establishment of a time regulation 
institute and its impact on the characters' lives.</code></pre>
<p>The output shows that the response was successfully mapped to the Java class <em>BookInfo</em>.</p>
<h2 id="bd-conclusion" data-id="conclusion">7. Conclusion</h2>
<div class="bd-anchor" id="conclusion"></div>
<p>In this article, we discussed LLM integration with the Apache Camel OpenAI component.</p>
<p>Firstly, we learned how to send prompts to an LLM using Camel routes and implemented a chat completion application based on them. Then, we saw how to change the OpenAI-compatible endpoint. We also learned to maintain conversational memory so that the model can remember previous prompts throughout a chat. Finally, we discussed formatting an LLM’s responses in a predefined format.</p>
<p>Besides the applications we discussed, there are other use cases. For example, we can process an LLM’s responses chunk by chunk rather than waiting for the full completion of a response. We have to set the <em>streaming</em> option to <em>true</em>. Another example is the support for multi-modal inputs. We can use text files and images with vision-capable models.</p>
<p>As usual, the complete source code for the examples is available <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/eugenp/tutorials/tree/master/libraries-llms-2">over on GitHub</a>.</p><p>The post <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/llm-integration-apache-camel-openai-component">LLM Integration With Apache Camel OpenAI Component</a> first appeared on <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com">Baeldung</a>.</p><Img align="left" border="0" height="1" width="1" alt="" style="border:0;float:left;margin:0;padding:0;width:1px!important;height:1px!important;" hspace="0" src="https://feeds.feedblitz.com/~/i/964742375/0/baeldung">
<div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/964742375/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/964742375/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f07%2fJava-Featured-12-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/964742375/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/964742375/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/964742375/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/llm-integration-apache-camel-openai-component#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/llm-integration-apache-camel-openai-component/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</content:encoded>
					
					<wfw:commentRss>https://feeds.feedblitz.com/~/964742375/0/baeldung/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<webfeeds:featuredImage>https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-150x150.jpg</webfeeds:featuredImage></item>
<item>
<feedburner:origLink>https://www.baeldung.com/java-ahead-of-time-cache</feedburner:origLink>
		<title>A Guide to Ahead-of-Time Cache in the Java</title>
		<link>https://feeds.feedblitz.com/~/964742378/0/baeldung</link>
					<comments>https://feeds.feedblitz.com/~/964742378/0/baeldung#respond</comments>
		
		<dc:creator><![CDATA[Bhaskar Ghosh]]></dc:creator>
		<pubDate>Thu, 30 Jul 2026 23:21:22 +0000</pubDate>
				<category><![CDATA[JVM]]></category>
		<category><![CDATA[>= Java 26]]></category>
		<category><![CDATA[popular]]></category>
		<guid isPermaLink="false">https://www.baeldung.com/?p=204500</guid>
					<description><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="max-width:100% !important;height:auto !important;float: left; margin-right: 5px;" loading="lazy" /><p>Learn how to use Ahead-of-Time (AOT) cache to reuse a build's output, so your app can start faster in the subsequent excecutions.</p>
<p>The post <a rel="NOFOLLOW" href="https://feeds.feedblitz.com/~/964742378/0/baeldung">A Guide to Ahead-of-Time Cache in the Java</a> first appeared on <a rel="NOFOLLOW" href="https://www.baeldung.com">Baeldung</a>.</p><div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/964742378/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/964742378/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f07%2fJava-Featured-12-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/964742378/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/964742378/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/964742378/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/java-ahead-of-time-cache#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/java-ahead-of-time-cache/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</description>
										<content:encoded><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="float: left; margin-right: 5px;" decoding="async" loading="lazy" srcset="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-1024x536.jpg 1024w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-300x157.jpg 300w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-768x402.jpg 768w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-100x52.jpg 100w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12.jpg 1200w" sizes="auto, (max-width: 580px) 100vw, 580px" /><h2 id="bd-introduction" data-id="introduction">1. Introduction</h2><div class="bd-anchor" id="introduction"></div>
<p>Every time we start a Java app, the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/jvm-series">JVM</a> rebuilds the same classes from scratch, which shows up in the startup time. The Ahead-of-Time (AOT) cache lets us use caching to reuse the build&#8217;s output, so later runs start faster. In this tutorial, we&#8217;ll use the AOT cache on a sample app. We&#8217;ll build a cache for the app, confirm that the JVM really uses the AOT cache, and then train the cache. We&#8217;ll use JDK 26 for this tutorial.</p>
<h2 id="bd-what-is-the-ahead-of-time-cache-in-java" data-id="what-is-the-ahead-of-time-cache-in-java">2. What Is the Ahead-of-Time Cache in Java</h2><div class="bd-anchor" id="what-is-the-ahead-of-time-cache-in-java"></div>
<p>The feature first arrived in JDK 24 through <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://openjdk.org/jeps/483">JEP 483</a>, as the first piece of Project Leyden. <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/java-25-features">JDK 25</a> added method profiles so the JIT can warm up sooner, and JDK 26 lifted the old garbage-collector limit through <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://openjdk.org/jeps/516">JEP 516</a>. In the following sections we look at how the cache achieves what it claims. Let&#8217;s consider a small catalog service that we already have on hand: its code lives in one app.jar, packaged next to the handful of library JARs it depends on, and it boots through a <em>com.example.App</em> main class. <strong>We&#8217;ll build a cache for the app, confirm the JVM uses the cache, and then train well enough to see speed gains in its loading time.</strong></p>
<h3 id="bd-1-how-the-cache-speeds-up-startup" data-id="1-how-the-cache-speeds-up-startup">2.1. How the Cache Speeds Up Startup</h3><div class="bd-anchor" id="1-how-the-cache-speeds-up-startup"></div>
<p>Normally the JVM reads, loads, and links each class as the app boots, and it repeats that work on every run.<strong> The cache changes this by saving those classes once, so that on the next run they come straight from disk instead of being rebuilt</strong>. Because our cache captures the work of one specific app, we reuse it whenever we run that same app. The speedup comes from moving the heavy lifting earlier, which leaves the classes ready the moment the program starts.</p>
<h3 id="bd-2-training-and-production-runs" data-id="2-training-and-production-runs">2.2. Training and Production Runs</h3><div class="bd-anchor" id="2-training-and-production-runs"></div>
<p>Before we discuss the caching workflow, it helps to discuss two words we&#8217;ll lean on throughout: training and production. <strong>A training run is one we carry out deliberately, ahead of time, to watch the app and build the cache from what we observe, so it never serves real users. </strong>A production run, by contrast, is the real deployment that serves traffic, and it loads the finished cache to start faster.</p>
<h3 id="bd-2-the-three-phase-workflow" data-id="2-the-three-phase-workflow">2.2. The Three-Phase Workflow</h3><div class="bd-anchor" id="2-the-three-phase-workflow"></div>
<p><strong>The original workflow of the caching process runs in three phases, namely record, create, and run.</strong> Each step feeds the next. We start with the record phase. Here the JVM watches the app and writes what it sees to a configuration file. We switch it on with a flag and name the file through a configuration:</p>
<pre><code class="language-java">java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf \
  -cp app.jar com.example.App ...</code></pre>
<p>As it runs, this phase notes which classes load and which methods turn hot, then stores those findings in <em>app.aotconf</em>. Once the file is created, the next step comes to effect. We feed the file into the create phase, which turns the recorded data into a real cache:</p>
<pre><code class="language-java">java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf \
  -XX:AOTCache=app.aot -cp app.jar</code></pre>
<p>Once the cache exists, the run phase simply points at it and benefits from the faster start:</p>
<pre><code class="language-java">java -XX:AOTCache=app.aot -cp app.jar com.example.App ...</code></pre>
<p>We observe here that since the classes are already prepared, this final run skips the work the first two phases did. In the three phases discussed above, the record and create steps make up the training side, while the run phase stands in for production.</p>
<h2 id="bd-how-to-build-the-cache" data-id="how-to-build-the-cache">3. How to Build the Cache</h2><div class="bd-anchor" id="how-to-build-the-cache"></div>
<p>We should now focus on building the cache. Running the three phase workflow everytime is not very developer friendly. Also, it&#8217;s more than most apps need. In the following sections, <strong>we discuss how JDK 25 folded and converged the cache building steps.</strong></p>
<h3 id="bd-1-making-the-cache-in-one-step" data-id="1-making-the-cache-in-one-step">3.1. Making the Cache in One Step</h3><div class="bd-anchor" id="1-making-the-cache-in-one-step"></div>
<p>We hand a destination to <em>-XX:AOTCacheOutput</em>, and the JVM builds the cache for us. It shuts down and outputs it in the mentioned destination:</p>
<pre><code class="language-java">java -XX:AOTCacheOutput=app.aot \
  -cp app.jar com.example.App ...</code></pre>
<p>If we&#8217;d rather keep the training command separate from deployment, JEP 514 offers a two-step variant that records once and then reuses the cache:</p>
<pre><code class="language-java">java -XX:AOTCacheOutput=app.aot \
  -cp app.jar com.example.App ...
java -XX:AOTCache=app.aot -cp app.jar com.example.App ...</code></pre>
<p>For the majority of apps, this shorter path is what is used.</p>
<h3 id="bd-2-splitting-the-work-for-small-machines" data-id="2-splitting-the-work-for-small-machines">3.2. Splitting the Work for Small Machines</h3><div class="bd-anchor" id="2-splitting-the-work-for-small-machines"></div>
<p>Convenient as the one-step path is, it comes with a memory bill. <strong>While it assembles the cache, the JVM spins up a second heap the same size as the training heap, so the peak demand climbs to roughly twice the size we set.</strong> With <em>-Xms2g -Xmx2g</em>, for instance, the machine needs about 4 GB before it finishes. <strong>That overhead is why the three-phase split still earns its place on small cloud servers.</strong> We can record on a modest instance that mirrors production, and then assemble the cache on a larger one where there&#8217;s room to spare. The recording stays faithful to the real environment, while the heavy assembly runs where memory is plentiful.</p>
<h2 id="bd-how-to-shape-the-cache-our-app-needs" data-id="how-to-shape-the-cache-our-app-needs">4. How to Shape the Cache Our App Needs</h2><div class="bd-anchor" id="how-to-shape-the-cache-our-app-needs"></div>
<p><strong>A cache only pays off when the training run and the production are in line</strong>. And this require a handful of conditions to be satisfied.</p>
<h3 id="bd-1-keeping-training-and-production-aligned" data-id="1-keeping-training-and-production-aligned">4.1. Keeping Training and Production aligned</h3><div class="bd-anchor" id="1-keeping-training-and-production-aligned"></div>
<p>Let&#8217;s see a few rules which ensure the training and production runs align:</p>
<ul>
<li>the JAR timestamps stay the same across training runs</li>
<li>both runs use the same JDK, hardware, and OS</li>
<li>the training run acts like the real one, so the busy paths match</li>
<li>the classpath is a list of plain JARs, with no folders, wildcards, or nested JARs</li>
<li>the production classpath holds at least what the training one holds</li>
<li>no JVMTI agent adds to the boot or system class search at run time</li>
</ul>
<p><strong>Following the above, we should ideally see the deployment run produce the same result as training, and perhaps faster.</strong></p>
<h3 id="bd-2-checking-that-the-cache-is-valid" data-id="2-checking-that-the-cache-is-valid">4.2. Checking That the Cache Is Valid</h3><div class="bd-anchor" id="2-checking-that-the-cache-is-valid"></div>
<p>Rather than trusting the cache blindly, we can ask the JVM to treat a broken setup as a hard failure. Adding <em>-XX:AOTMode=on</em> does exactly that, turning a silent fallback into a clear error:</p>
<pre><code class="language-java">java -XX:AOTCache=app.aot -XX:AOTMode=on \
  -cp app.jar com.example.App ...</code></pre>
<p>So if the cache goes missing, or one of the conditions above slips, the run stops instead of quietly dropping the speedup. <strong>It&#8217;s worth remembering, though, that the cache is valid for a single build only. </strong>A code change, a new or updated library, or a JDK upgrade each invalidate it. <strong>This means we regenerate the cache alongside every build.</strong> And when the expected gain fails to appear, we can trace what the cache loads by adding <em>-Xlog:aot,class+path=info</em>.</p>
<h2 id="bd-tips-for-good-training-runs" data-id="tips-for-good-training-runs">5. Tips for Good Training Runs</h2><div class="bd-anchor" id="tips-for-good-training-runs"></div>
<p>Of the three phases we&#8217;ve covered, the training run is the one that carries the real weight. The create and run phases are mostly mechanical: they package whatever the training run observed and replay it on startup, without judging whether that was the right thing to capture. So the cache is only ever as good as the run that built it. Let&#8217;s discuss a few tips for architecting a good training run.</p>
<h3 id="bd-1-matching-what-production-loads" data-id="1-matching-what-production-loads">5.1. Matching What Production Loads</h3><div class="bd-anchor" id="1-matching-what-production-loads"></div>
<p>There&#8217;s a natural trade-off between how realistic a training run is and how easy it is to stage. <strong>A true production run opens connections, queries the database, and writes logs, all of which are difficult to reproduce.</strong> Therefore a synthetic run that follows the same paths usually strikes the better balance. What matters most is overlap. <strong>The closer the training run loads the same classes as production, the better the startup we get.</strong> To see that overlap, we can list everything a run loads by adding<em> -verbose:class</em> when we launch it. <strong>A light smoke test gives us a tidy way to drive those results.</strong> It exercises a normal startup without pulling in heavy suites, which keeps the cache lean. We named it in BDD style while keeping the framework minimal:</p>
<pre><code class="language-java">class StartupSmokeTest {
    @Test
    void givenFreshApplication_whenMainPathRuns_thenCoreServicesInitialize() {
        App app = App.bootstrap();
        assertTrue(app.isReady());
        assertNotNull(app.catalogService());
    }
}</code></pre>
<p>We rely on tests like this for the common startup paths. Also, we are deliberately leaving the stress and regression suites out of the training run.</p>
<h3 id="bd-2-finding-hot-methods-with-jfr" data-id="2-finding-hot-methods-with-jfr">5.2. Finding Hot Methods With JFR</h3><div class="bd-anchor" id="2-finding-hot-methods-with-jfr"></div>
<p>Now and then a method matters at run time yet never surfaces during a shallow training run. <strong>The <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/java-flight-recorder-monitoring">JDK Flight Recorder</a> is how we catch those cases. </strong>We begin by enabling the class-load event, then record the app from the moment it starts. Let&#8217;s start by first enabling the event: <code class="language-java">jfr configure jdk.ClassLoad#enabled=true
</code> Now, let&#8217;s start the recording: <code class="language-java">java -XX:StartFlightRecording:settings=custom.jfc,duration=60s,filename=/tmp/AOT.jfr</code> With a recording in hand, we can check the loaded classes and the hottest methods:</p>
<pre><code class="language-java">jfr print --events "jdk.ClassLoad" /tmp/AOT.jfr
jfr view hot-methods /tmp/AOT.jfr</code></pre>
<p><strong>If a hot method turns out to be missing, we extend the smoke test to walk through it. We start by leaning on a temporary folder, then a local network, or a mocked database wherever the path demands one.</strong></p>
<h2 id="bd-best-practices" data-id="best-practices">6. Best Practices</h2><div class="bd-anchor" id="best-practices"></div>
<p>A healthy cache really comes down to a few recurring habits and practices:</p>
<ul>
<li>Validity, since every rebuild or JDK upgrade calls for a fresh cache</li>
<li>Portability, since the cache is bound to one JVM and platform</li>
<li>Coverage, since the training run has to walk the normal startup paths</li>
<li>Setup, since both the JAR and the cache should run with least privilege</li>
</ul>
<h2 id="bd-conclusion" data-id="conclusion">7. Conclusion</h2><div class="bd-anchor" id="conclusion"></div>
<p>In this tutorial, we followed the whole AOT cache promise. We built a cache both in a single step and across three phases. Additionally, we confirmed that the JVM was really using it, and then trained it with a light smoke test so it covered the paths that count. Taken together, these steps make the cache easy to add and just as easy to keep current.</p><p>The post <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/java-ahead-of-time-cache">A Guide to Ahead-of-Time Cache in the Java</a> first appeared on <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com">Baeldung</a>.</p><Img align="left" border="0" height="1" width="1" alt="" style="border:0;float:left;margin:0;padding:0;width:1px!important;height:1px!important;" hspace="0" src="https://feeds.feedblitz.com/~/i/964742378/0/baeldung">
<div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/964742378/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/964742378/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f07%2fJava-Featured-12-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/964742378/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/964742378/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/964742378/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/java-ahead-of-time-cache#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/java-ahead-of-time-cache/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</content:encoded>
					
					<wfw:commentRss>https://feeds.feedblitz.com/~/964742378/0/baeldung/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<webfeeds:featuredImage>https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-12-150x150.jpg</webfeeds:featuredImage></item>
<item>
<feedburner:origLink>https://www.baeldung.com/spring-ai-short-term-memory-sessions</feedburner:origLink>
		<title>A Guide to Short-Term Memory Sessions in Spring AI</title>
		<link>https://feeds.feedblitz.com/~/964742381/0/baeldung</link>
					<comments>https://feeds.feedblitz.com/~/964742381/0/baeldung#respond</comments>
		
		<dc:creator><![CDATA[Yadier Betancourt]]></dc:creator>
		<pubDate>Thu, 30 Jul 2026 23:16:25 +0000</pubDate>
				<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[Spring AI ChatClient]]></category>
		<guid isPermaLink="false">https://www.baeldung.com/?p=204323</guid>
					<description><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-14-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="Two co-workers; the one on the left is holding up a magnifying glass to an artistic representation of files and folders. The one on the right is looking at a projection of some Java code. In between them there is an office plant. To the left of the image are the words &quot;Java on Baeldung&quot;" style="max-width:100% !important;height:auto !important;float: left; margin-right: 5px;" loading="lazy" /><p>Discover Spring AI Session, an event-sourced short-term memory layer for managing conversation history efficiently.</p>
<p>The post <a rel="NOFOLLOW" href="https://feeds.feedblitz.com/~/964742381/0/baeldung">A Guide to Short-Term Memory Sessions in Spring AI</a> first appeared on <a rel="NOFOLLOW" href="https://www.baeldung.com">Baeldung</a>.</p><div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/964742381/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/964742381/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f07%2fJava-Featured-14-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/964742381/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/964742381/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/964742381/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/spring-ai-short-term-memory-sessions#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/spring-ai-short-term-memory-sessions/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</description>
										<content:encoded><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-14-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="Two co-workers; the one on the left is holding up a magnifying glass to an artistic representation of files and folders. The one on the right is looking at a projection of some Java code. In between them there is an office plant. To the left of the image are the words &quot;Java on Baeldung&quot;" style="float: left; margin-right: 5px;" decoding="async" loading="lazy" srcset="https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-14-1024x536.jpg 1024w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-14-300x157.jpg 300w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-14-768x402.jpg 768w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-14-100x52.jpg 100w, https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-14.jpg 1200w" sizes="auto, (max-width: 580px) 100vw, 580px" /><h2 id="bd-overview" data-id="overview">1. Overview</h2>
<div class="bd-anchor" id="overview"></div>
<p>Large language models are stateless, so every request we send is independent unless we replay the earlier conversation ourselves. Spring AI has traditionally solved this with <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-ai-chat-memory"><em>ChatMemory</em></a>, but as conversations grow, naively replaying every message quickly overflows the model&#8217;s context window.</p>
<p>In this tutorial, we&#8217;ll explore Spring AI Session, an event-sourced short-term memory layer that stores conversation history and shrinks it intelligently when it gets too large. We&#8217;ll create and inspect sessions, plug memory into a <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-ai-chatclient"><em>ChatClient</em></a> through an <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-ai-advisors">advisor</a>, and review the available compaction strategies.</p>
<h2 id="bd-what-are-memory-sessions" data-id="what-are-memory-sessions">2. What Are Memory Sessions?</h2>
<div class="bd-anchor" id="what-are-memory-sessions"></div>
<p><strong>A session is a container for a single conversation, identified by an ID and optionally tied to a user.</strong> Instead of storing a flat list of messages, it records an ordered stream of <em>SessionEvent</em> objects, each wrapping a <em>Message</em> with a timestamp, a unique ID, and optional branch information.</p>
<p>The library groups these events into <em>turns</em>. <strong>A turn is one user message plus every assistant reply, tool call, and tool result that follows it, until the next user message.</strong> Turns are the atomic unit the library never breaks apart.</p>
<p>That last point is the key improvement over the older <em>ChatMemory</em> API. <strong>When history grows too large, Spring AI Session compacts it along turn boundaries rather than evicting the oldest individual messages.</strong> This way, we never end up with a dangling tool call whose result was dropped. For reference, a <em>MessageWindowChatMemory</em> capped at 20 messages becomes a <em>TurnCountTrigger(20)</em> paired with a sliding-window strategy.</p>
<p>Every event is immutable and timestamped. This lets the same session support more advanced scenarios, such as isolating the histories of cooperating agents through branch labels.</p>
<p>Sessions are currently incubating in the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/spring-ai-community/spring-ai-session"><em>spring-ai-community</em></a> project and are slated to replace <em>ChatMemory</em> in a future Spring AI release.</p>
<h2 id="bd-setting-up-the-project" data-id="setting-up-the-project">3. Setting Up the Project</h2>
<div class="bd-anchor" id="setting-up-the-project"></div>
<p><strong>The Session API requires Spring AI 2.x and Spring Boot 4.x.</strong> We add the core module, which ships the in-memory repository and all the compaction building blocks:</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.springaicommunity&lt;/groupId&gt;
    &lt;artifactId&gt;spring-ai-session-management&lt;/artifactId&gt;
    &lt;version&gt;0.5.0&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p>The latest version of <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.springaicommunity/spring-ai-session-management"><em>spring-ai-session-management</em></a> is available in the Maven Repository.</p>
<p>To talk to a model, we add a Spring AI chat starter. The session layer is provider-agnostic, so OpenAI, Anthropic, or a local model works just as well. Here, we use <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-starter-model-google-genai">spring-ai-starter-model-google-genai</a> with our Gemini API key:</p>
<pre><code class="language-properties">spring.ai.google.genai.api-key=${GEMINI_API_KEY}
spring.ai.google.genai.chat.options.model=gemini-3.5-flash
</code></pre>
<p>For production, we&#8217;d typically swap the in-memory store for a relational one. Adding the <em>spring-ai-starter-session-jdbc</em> starter auto-configures a JDBC repository for PostgreSQL, MySQL, MariaDB, or H2. In this tutorial, we&#8217;ll stick with the in-memory repository to keep the examples self-contained.</p>
<h2 id="bd-creating-and-managing-sessions" data-id="creating-and-managing-sessions">4. Creating and Managing Sessions</h2>
<div class="bd-anchor" id="creating-and-managing-sessions"></div>
<p><strong>A <em>SessionService</em> is the entry point for the whole API.</strong> We start by exposing it as a bean backed by an <em>InMemorySessionRepository</em>:</p>
<pre><code class="language-java">@Bean
public SessionService sessionService() {
    return DefaultSessionService.builder()
      .sessionRepository(InMemorySessionRepository.builder().build())
      .build();
}
</code></pre>
<p>Let&#8217;s create a session, append a couple of messages, and read them back as plain Spring AI messages:</p>
<pre><code class="language-java">@Test
void givenSession_whenAppendingMessages_thenStoredInOrder() {
    Session session = sessionService.create(CreateSessionRequest.builder()
      .userId("alice")
      .build());
    sessionService.appendMessage(session.id(), new UserMessage("What is Spring AI?"));
    sessionService.appendMessage(session.id(),
      new AssistantMessage("It's an application framework for AI engineering."));
    List&lt;Message&gt; messages = sessionService.getMessages(session.id());
    assertThat(messages).hasSize(2);
    assertThat(messages.get(0).getMessageType()).isEqualTo(MessageType.USER);
    assertThat(messages.get(1).getMessageType()).isEqualTo(MessageType.ASSISTANT);
}
</code></pre>
<p>For lower-level access, <em>getEvents()</em> returns the richer <em>SessionEvent</em> stream, including timestamps and metadata. The service also lets us look up a conversation later with <em>findById()</em> or list everything for a user through <em>findByUserId()</em>. Finally, we can set a time-to-live on <em>CreateSessionRequest</em> so stale sessions expire automatically.</p>
<h2 id="bd-using-the-sessionmemoryadvisor" data-id="using-the-sessionmemoryadvisor">5. Using the SessionMemoryAdvisor</h2>
<div class="bd-anchor" id="using-the-sessionmemoryadvisor"></div>
<p>Managing the service by hand is useful, but most applications want memory to work transparently, much like the advisors we lean on when building an <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-ai-assistant">AI assistant</a>. <strong>The <em>SessionMemoryAdvisor</em> hooks the session into the <em>ChatClient</em> pipeline</strong>, loading history before each call and appending the new exchange afterward. It&#8217;s a standard Spring AI advisor.</p>
<h3 id="bd-1-configuring-the-advisor-bean" data-id="1-configuring-the-advisor-bean">5.1. Configuring the Advisor Bean</h3>
<div class="bd-anchor" id="1-configuring-the-advisor-bean"></div>
<p>Now, let&#8217;s expose the advisor as a bean, attaching a compaction trigger and strategy:</p>
<pre><code class="language-java">@Bean
public SessionMemoryAdvisor sessionMemoryAdvisor(SessionService sessionService) {
    return SessionMemoryAdvisor.builder(sessionService)
      .defaultUserId("alice")
      .compactionTrigger(new TurnCountTrigger(20))
      .compactionStrategy(SlidingWindowCompactionStrategy.builder()
          .maxEvents(10)
          .build())
      .build();
}
</code></pre>
<p><strong>This configuration keeps the ten most recent events and compacts once a session passes twenty turns.</strong></p>
<h3 id="bd-2-wiring-it-into-chatclient" data-id="2-wiring-it-into-chatclient">5.2. Wiring It Into ChatClient</h3>
<div class="bd-anchor" id="2-wiring-it-into-chatclient"></div>
<p>Next, we register the advisor as a default on a <em>ChatClient</em>, then identify the conversation with the session-ID parameter on each call:</p>
<pre><code class="language-java">@Component
public class ChatService {
    private final ChatClient chatClient;
    public ChatService(ChatModel chatModel, SessionMemoryAdvisor sessionMemoryAdvisor) {
        this.chatClient = ChatClient.builder(chatModel)
          .defaultAdvisors(sessionMemoryAdvisor)
          .build();
    }
    public String chat(String sessionId, String prompt) {
        return chatClient.prompt()
          .user(prompt)
          .advisors(a -&gt; a.param(SessionMemoryAdvisor.SESSION_ID_CONTEXT_KEY, sessionId))
          .call()
          .content();
    }
}
</code></pre>
<p>The advisor stores every exchange under that session ID, so a follow-up question resolves against the earlier context. In the test below, we share a name in the first message, then ask for it back in a second one. The assertion confirms the reply still recalls the name, proving the session preserved context across the two turns:</p>
<pre><code class="language-java">@Test
void givenSessionId_whenChattingAcrossTurns_thenContextIsRemembered() {
    chatService.chat("session-abc", "My name is Yadier, remember it.");
    String response = chatService.chat("session-abc", "What is my name?");
    assertThat(response).containsIgnoringCase("Yadier");
    assertThat(sessionService.getMessages("session-abc")).hasSize(4);
}
</code></pre>
<p>The second call answers with the remembered name, which confirms the advisor replayed the first turn from the session before calling the model.</p>
<h2 id="bd-compaction-strategies" data-id="compaction-strategies">6. Compaction Strategies</h2>
<div class="bd-anchor" id="compaction-strategies"></div>
<p><strong>A compaction trigger decides <em>when</em> to shrink history, while a strategy decides <em>how</em>.</strong> Besides <em>TurnCountTrigger</em>, we can trigger on estimated tokens with <em>TokenCountTrigger</em>, or combine conditions with <em>CompositeCompactionTrigger</em>.</p>
<h3 id="bd-1-comparing-the-strategies" data-id="1-comparing-the-strategies">6.1. Comparing the Strategies</h3>
<div class="bd-anchor" id="1-comparing-the-strategies"></div>
<p>The library ships four strategies, all of which respect turn boundaries:</p>
<table class="table-styled">
<thead>
<tr>
<th>Strategy</th>
<th>LLM required</th>
<th>Best for</th>
</tr>
</thead>
<tbody>
<tr>
<td><em>SlidingWindowCompactionStrategy</em></td>
<td>No</td>
<td>Cost-sensitive, recent-context chats</td>
</tr>
<tr>
<td><em>TurnWindowCompactionStrategy</em></td>
<td>No</td>
<td>Keeping the last N complete turns</td>
</tr>
<tr>
<td><em>TokenCountCompactionStrategy</em></td>
<td>No</td>
<td>Hard context-window limits</td>
</tr>
<tr>
<td><em>RecursiveSummarizationCompactionStrategy</em></td>
<td>Yes</td>
<td>Long-running sessions needing recall</td>
</tr>
</tbody>
</table>
<p>The first three strategies drop older events outright, so they&#8217;re fast and free — a good default when we just need to cap history cheaply. Recursive summarization instead preserves the gist of what it removes, at the cost of an extra model call. In short, <strong>we reach for recursive summarization only when we need to recall older context,</strong> otherwise, the cheaper windowing strategies are enough.</p>
<h3 id="bd-2-running-compaction-manually" data-id="2-running-compaction-manually">6.2. Running Compaction Manually</h3>
<div class="bd-anchor" id="2-running-compaction-manually"></div>
<p><strong>The <em>SessionMemoryAdvisor</em> runs compaction automatically as a session grows, but we can also trigger it ourselves by calling <em>compact()</em> directly.</strong> That&#8217;s handy in tests or batch jobs where we decide exactly when history shrinks. Let&#8217;s use the <em>SlidingWindowCompactionStrategy</em> to keep only the most recent events, passing it and a trigger to <em>compact()</em>, which returns a <em>CompactionResult</em> describing what it archived:</p>
<pre><code class="language-java">@Test
void givenMultiTurnConversation_whenCompacting_thenOlderEventsAreArchived() {
    Session session = sessionService.create(CreateSessionRequest.builder()
      .userId("alice")
      .build());
    for (int turn = 1; turn &lt;= 4; turn++) {
        sessionService.appendMessage(session.id(), new UserMessage("Question " + turn));
        sessionService.appendMessage(session.id(), new AssistantMessage("Answer " + turn));
    }
    CompactionResult result = sessionService.compact(session.id(),
      new TurnCountTrigger(2),
      SlidingWindowCompactionStrategy.builder()
        .maxEvents(4)
        .build());
    assertThat(result.eventsRemoved()).isPositive();
    assertThat(sessionService.getEvents(session.id())).hasSameSizeAs(result.compactedEvents());
}
</code></pre>
<p>First we build up four turns, then we compact the session. Finally, we verify that some events were removed and the stored history now matches the compacted result. The assertions confirm that older events were archived and that the session now holds only the compacted set.</p>
<h3 id="bd-3-summarizing-with-an-llm" data-id="3-summarizing-with-an-llm">6.3. Summarizing With an LLM</h3>
<div class="bd-anchor" id="3-summarizing-with-an-llm"></div>
<p><strong>Recursive summarization is the one strategy that needs a <em>ChatClient</em>.</strong> The other three simply drop or window events, but this one asks a model to write a summary of the events it removes, then replaces them with a single synthetic summary event. That extra model call is the price for keeping older context available in condensed form:</p>
<pre><code class="language-java">@Test
void givenLongConversation_whenSummarizing_thenOlderEventsAreReplacedBySummary() {
    Session session = sessionService.create(CreateSessionRequest.builder()
      .userId("alice")
      .build());
    for (int turn = 1; turn &lt;= 4; turn++) {
        sessionService.appendMessage(session.id(), new UserMessage("Question " + turn));
        sessionService.appendMessage(session.id(), new AssistantMessage("Answer " + turn));
    }
    ChatClient chatClient = ChatClient.builder(chatModel).build();
    CompactionResult result = sessionService.compact(session.id(),
      new TurnCountTrigger(2),
      RecursiveSummarizationCompactionStrategy.builder(chatClient)
        .maxEventsToKeep(4)
        .build());
    SessionEvent summary = result.compactedEvents().stream()
      .filter(SessionEvent::isSynthetic)
      .findFirst()
      .orElseThrow();
    assertThat(result.eventsRemoved()).isPositive();
    assertThat(summary.getMessage().getText()).isNotBlank();
}
</code></pre>
<p>The synthetic event in the compacted set is the model-generated summary, so the session keeps the gist of the dropped turns in condensed form.</p>
<h2 id="bd-conclusion" data-id="conclusion">7. Conclusion</h2>
<div class="bd-anchor" id="conclusion"></div>
<p>In this article, we explored Spring AI&#8217;s short-term memory sessions. We saw how a <em>SessionService</em> stores conversations as turn-aware events. The <em>SessionMemoryAdvisor</em> makes that memory transparent to the <em>ChatClient</em>. Finally, pluggable triggers and strategies keep history within the model&#8217;s context window.</p>
<p>As the API graduates from incubation, it&#8217;s positioned to become the default replacement for <em>ChatMemory</em>, so investing in it now sets us up for the long term.</p>
<p>As always, the full source code is available over on <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/eugenp/tutorials/tree/master/spring-ai-modules/spring-ai-session">GitHub</a>.</p><p>The post <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-ai-short-term-memory-sessions">A Guide to Short-Term Memory Sessions in Spring AI</a> first appeared on <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com">Baeldung</a>.</p><Img align="left" border="0" height="1" width="1" alt="" style="border:0;float:left;margin:0;padding:0;width:1px!important;height:1px!important;" hspace="0" src="https://feeds.feedblitz.com/~/i/964742381/0/baeldung">
<div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/964742381/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/964742381/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f07%2fJava-Featured-14-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/964742381/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/964742381/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/964742381/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/spring-ai-short-term-memory-sessions#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/spring-ai-short-term-memory-sessions/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</content:encoded>
					
					<wfw:commentRss>https://feeds.feedblitz.com/~/964742381/0/baeldung/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<webfeeds:featuredImage>https://www.baeldung.com/wp-content/uploads/2024/07/Java-Featured-14-150x150.jpg</webfeeds:featuredImage></item>
<item>
<feedburner:origLink>https://www.baeldung.com/spring-application-startup-tracking</feedburner:origLink>
		<title>Application Startup Tracking in Spring</title>
		<link>https://feeds.feedblitz.com/~/963082187/0/baeldung</link>
					<comments>https://feeds.feedblitz.com/~/963082187/0/baeldung#respond</comments>
		
		<dc:creator><![CDATA[Michael Krimgen]]></dc:creator>
		<pubDate>Sat, 25 Jul 2026 19:15:03 +0000</pubDate>
				<category><![CDATA[Spring Boot]]></category>
		<category><![CDATA[Java Flight Recorder (JFR)]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[popular]]></category>
		<category><![CDATA[Spring Boot Actuator]]></category>
		<guid isPermaLink="false">https://www.baeldung.com/spring-application-startup-tracking</guid>
					<description><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Featured-Image-09-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="max-width:100% !important;height:auto !important;float: left; margin-right: 5px;" loading="lazy" /><p>Learn how to track and analyze Spring application startup with <em>ApplicationStartup</em>, <em>BufferingApplicationStartup</em>, <em>FlightRecorderApplicationStartup</em>, and custom startup steps.</p>
<p>The post <a rel="NOFOLLOW" href="https://feeds.feedblitz.com/~/963082187/0/baeldung">Application Startup Tracking in Spring</a> first appeared on <a rel="NOFOLLOW" href="https://www.baeldung.com">Baeldung</a>.</p><div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/963082187/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/963082187/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f11%2fSpring-Featured-Image-09-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/963082187/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/963082187/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/963082187/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/spring-application-startup-tracking#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/spring-application-startup-tracking/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</description>
										<content:encoded><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Featured-Image-09-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="float: left; margin-right: 5px;" decoding="async" loading="lazy" srcset="https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Featured-Image-09-1024x536.jpg 1024w, https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Featured-Image-09-300x157.jpg 300w, https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Featured-Image-09-768x402.jpg 768w, https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Featured-Image-09-100x52.jpg 100w, https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Featured-Image-09-600x314.jpg 600w, https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Featured-Image-09.jpg 1200w" sizes="auto, (max-width: 580px) 100vw, 580px" /><h2 id="bd-introduction" data-id="introduction">1. Introduction</h2>
<div class="bd-anchor" id="introduction"></div>
<div>
<p>Application startup aims to speed up overall performance post-load. This means creating all necessary objects for initial use, but also caching or preparing data that&#8217;s highly likely to be required. In addition to being a way to improve the user experience, application startup may be problematic.</p>
<p>In this tutorial, we&#8217;ll learn how to facilitate, track, and monitor the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-application-context">context</a> creation of a Spring application.</p>
<p>First, we&#8217;ll recap what the Spring context is and why we want to track its creation. After that, we&#8217;ll look at the three implementations that Spring Boot provides to easily monitor context creation. Finally, we&#8217;ll take a quick look at how and why we might want to implement custom startup tracking logic.</p>
<h2 id="bd-spring-application-context" data-id="spring-application-context">2. Spring Application Context</h2>
<div class="bd-anchor" id="spring-application-context"></div>
<p>The Spring Boot application context serves as the Inversion of Control (IoC) container. <strong>This container manages the lifecycle of a Spring application and provides dependency injection, Spring bean management, events, and configuration management</strong>.</p>
<p>The context is created during the startup of a Spring application. For simple applications, a Spring application typically starts quickly, but can take longer for complex ones and even freeze during initialization.</p>
<p>There are various ways to <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-boot-startup-speed">improve the startup time</a>. <strong>To help us better understand the startup process, Spring provides the ability to track the application context creation</strong>.</p>
<h2 id="bd-the-applicationstartup-interface" data-id="the-applicationstartup-interface">3. The <em>ApplicationStartup</em> Interface</h2>
<div class="bd-anchor" id="the-applicationstartup-interface"></div>
<p>Spring provides two implementations of the <em>ApplicationStartup</em> interface:</p>
<ul>
<li><em>DefaultApplicationStartup</em></li>
<li><em>FlightRecorderApplicationStartup</em></li>
</ul>
<p>The default implementation is a no-op, while <em>FlightRecorderApplicationStartup</em> provides an implementation for use with the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/java-flight-recorder-monitoring">Java Flight Recorder (JFR)</a>.</p>
<p>In addition, Spring Boot comes with the <em>BufferingApplicationStartup</em> implementation that records startup events in memory. <strong>If we want to use anything other than <em>DefaultApplicationStartup</em>, we need to initialize the Spring application with the desired <em>ApplicationStartup</em> implementation before we start the application</strong>.</p>
<h2 id="bd-bufferingapplicationstartup" data-id="bufferingapplicationstartup">4. <em>BufferingApplicationStartup</em></h2>
<div class="bd-anchor" id="bufferingapplicationstartup"></div>
<p>Let&#8217;s look at <em>BufferingApplicationStartup</em>, an implementation that stores startup events in memory and also provides an actuator endpoint for convenient access to the recorded events.</p>
<h3 id="bd-1-configuration" data-id="1-configuration">4.1. Configuration</h3>
<div class="bd-anchor" id="1-configuration"></div>
<p>First, we need to register <em>BufferingApplicationStartup</em> with the Spring Boot application:</p>
<pre><code class="language-java">@SpringBootApplication
public class ApplicationStartupTrackingApplication {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(ApplicationStartupTrackingApplication.class);
        app.setApplicationStartup(new BufferingApplicationStartup(2048));
        app.run(args);
    }
}</code></pre>
<p>The constructor parameter (<em>2048</em> in this example) specifies the buffer size, i.e., the maximum number of events that can be recorded.</p>
</div>
<h3 id="bd-2-access-via-the-actuator-endpoint" data-id="2-access-via-the-actuator-endpoint">4.2. Access via the Actuator Endpoint</h3>
<div class="bd-anchor" id="2-access-via-the-actuator-endpoint"></div>
<p>One way to retrieve the recorded events is via an actuator endpoint. Specifically, we can enable the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-boot-actuator-startup">startup actuator</a> in the application properties:</p>
<pre><code class="language-yaml">management.endpoints.web.exposure.include=startup</code></pre>
<p>In case we have a Spring bean, we also make the necessary addition:</p>
<pre><code class="language-java">@Service
public class SpecialService {
}</code></pre>
<p>If we access the actuator, we can see an entry for the service bean:</p>
<pre><code class="language-bash">curl localhost:8080/actuator/startup | jq
{
  "duration": "PT0.000754S",
  "endTime": "2026-07-19T18:31:27.493778Z",
  "startTime": "2026-07-19T18:31:27.493024Z",
  "startupStep": {
    "id": 107,
    "name": "spring.beans.instantiate",
    "parentId": 4,
    "tags": [
      {
        "key": "beanName",
        "value": "specialService"
      }
    ]
  }
}</code></pre>
<p><strong>It&#8217;s important to note that the actuator only works with <em>BufferingApplicationStartup</em></strong>. If we configure the application with any other implementation of the <em>ApplicationStartup</em> interface, we see an error message:</p>
<pre><code class="language-bash">curl localhost:8080/actuator/startup | jq
{
  "timestamp": "2026-07-19T14:37:45.989Z",
  "status": 404,
  "error": "Not Found",
  "path": "/actuator/startup"
}</code></pre>
<p><strong>Furthermore, we can call the actuator endpoint with a GET or a POST request</strong>. In both cases, the response should be the same; however, GET returns a snapshot of the timeline, while POST returns the snapshot and also clears the buffer.</p>
<h3 id="bd-3-access-via-the-application-context" data-id="3-access-via-the-application-context">4.3. Access via the Application Context</h3>
<div class="bd-anchor" id="3-access-via-the-application-context"></div>
<p>In addition to using the actuator endpoint, we can programmatically access the timeline <em>BufferingApplicationStartup</em> provides. To do so, we access the configured <em>BufferingApplicationStartup</em> instance:</p>
<pre><code class="language-java">@Component
public class StartupTracker {
    private BufferingApplicationStartup startup;
    
    public StartupTracker(@Autowired ApplicationContext context) {
        startup = (BufferingApplicationStartup) (
            (ConfigurableApplicationContext) context).getApplicationStartup();
    }
    public List recorded() {
        List recordedEvents = new ArrayList&lt;&gt;();
        for(StartupTimeline.TimelineEvent event : startup.getBufferedTimeline().getEvents()) {
            for(StartupStep.Tag tag : event.getStartupStep().getTags()) {
                recordedEvents.add(
                    event.getStartupStep().getName() + "  " +
                        tag.getKey() + " " +
                        tag.getValue()
                );
            }
        }
        return recordedEvents;
    }
}</code></pre>
<p>Thus, we have access to the deeper startup context.</p>
<h3 id="bd-4-usage-in-spring-boot-tests" data-id="4-usage-in-spring-boot-tests">4.4. Usage in Spring Boot Tests</h3>
<div class="bd-anchor" id="4-usage-in-spring-boot-tests"></div>
<p>This way, we can analyze the timeline and create a custom output format, filter certain events, and also test the implementation:</p>
<pre><code class="language-java">@SpringBootTest(useMainMethod = SpringBootTest.UseMainMethod.ALWAYS)
public class ApplicationTest {
    @Autowired
    private StartupTracker startupTracker;
    @Test
    void givenTheApplicationStarts_whenRetrieveRecordedEvents_ThenContainsCustomBeans() {
        Assertions.assertThat(startupTracker.recorded()).contains(
            "spring.beans.instantiate  beanName specialService",
            "spring.beans.instantiate  beanName startupTracker"
        );
    }
}</code></pre>
<p>We can see that the list of events includes the two beans that we have created, <em>SpecialService</em> and <em>StartupTracker.</em></p>
<p>Notably, we need to configure the test with <em>SpringBootTest.UseMainMethod.ALWAYS</em>. <strong>This ensures that we start the application via the main method and Spring Boot is configured to use <em>BufferingApplicationStartup</em> instead of the no-op implementation</strong>.</p>
<div>
<h2 id="bd-flightrecorderapplicationstartup" data-id="flightrecorderapplicationstartup">5. <em>FlightRecorderApplicationStartup</em></h2>
<div class="bd-anchor" id="flightrecorderapplicationstartup"></div>
<p>Another option to track application startup is <em>FlightRecorderApplicationStartup</em>, <strong>an implementation that provides Spring startup metrics in a format suitable for the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/java-flight-recorder-monitoring">Java Flight Recorder</a> (JFR), which can be viewed in the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/java-flight-recorder-view">Java Mission Control</a> (JMC) tool</strong>.</p>
<p>Let&#8217;s configure the application to use <em>FlightRecorderApplicationStartup</em>:</p>
<pre><code class="language-java">app.setApplicationStartup(new FlightRecorderApplicationStartup());</code></pre>
<p>In addition, we need to provide a JVM parameter:</p>
<pre><code class="language-bash">-XX:StartFlightRecording:filename=recording.jfr,duration=20s</code></pre>
<p>Now, Spring records the application startup events in a file named <em>recording.jfr</em> that we can view in the Java Mission Control (JMC) viewer:</p>
<p><a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/wp-content/uploads/2026/07/Scherm­afbeelding-2026-07-12-om-22.16.36.png"><img loading="lazy" decoding="async" class="alignnone wp-image-254416 size-large" src="https://www.baeldung.com/wp-content/uploads/2026/07/Scherm­afbeelding-2026-07-12-om-22.16.36-1024x406.png" alt="Java Mission control view" width="580" height="230" /></a></p>
<p>In particular, we can find the startup steps under the category <em>Spring Application</em>.</p>
<h2 id="bd-adding-custom-steps" data-id="adding-custom-steps">6. Adding Custom Steps</h2>
<div class="bd-anchor" id="adding-custom-steps"></div>
</div>
<p><strong>The recommended way to customize the startup timeline is to add custom startup steps to the <em>ApplicationStartup</em> instance</strong> that we can retrieve from the application context. To demonstrate how to do that, let&#8217;s create a service class and add some custom steps:</p>
<pre><code class="language-java">@Service
public class SpecialService {
    private ApplicationStartup applicationStartup;
    public SpecialService(ApplicationContext context) {
        this.applicationStartup = ((ConfigurableApplicationContext) context).getApplicationStartup();
    }
    @PostConstruct
    public void init() {
        StartupStep startupStep1 = this.applicationStartup.start("com.baeldung.special.service");
        try {
            startupStep1.tag("init", "connect to databases");
            // some long-running initialization
        } finally {
            startupStep1.end();
        }
        StartupStep startupStep2 = this.applicationStartup.start("com.baeldung.special.service");
        try {
            startupStep2.tag("init", "connect to AI agent");
            // more long-running initialization
        } finally {
            startupStep2.end();
        }
    }
}</code></pre>
<p>So, we can now see a step for the initialization of the <em>SpecialService</em> bean as the parent step (<em>id=107</em>) and the two additional steps (<em>id=[108,109]</em>) in the response of the actuator:</p>
<pre><code class="language-json">[
  {
    "duration": "PT0.000006S",
    "endTime": "2026-07-18T19:15:12.096227Z",
    "startTime": "2026-07-18T19:15:12.096221Z",
    "startupStep": {
      "id": 108,
      "name": "com.baeldung.special.service",
      "parentId": 107,
      "tags": [
        {
          "key": "init",
          "value": "connect to databases"
        }
      ]
    }
  },
  {
    "duration": "PT0.000002S",
    "endTime": "2026-07-18T19:15:12.096238Z",
    "startTime": "2026-07-18T19:15:12.096236Z",
    "startupStep": {
      "id": 109,
      "name": "com.baeldung.special.service",
      "parentId": 107,
      "tags": [
        {
          "key": "init",
          "value": "connect to AI agent"
        }
      ]
    }
  },
  {
    "duration": "PT0.000682S",
    "endTime": "2026-07-18T19:15:12.096328Z",
    "startTime": "2026-07-18T19:15:12.095646Z",
    "startupStep": {
      "id": 107,
      "name": "spring.beans.instantiate",
      "parentId": 4,
      "tags": [
        {
          "key": "beanName",
          "value": "specialService"
        }
      ]
    }
  }
]</code></pre>
<p>The output shows metrics for both startup steps: <em>connect to databases</em> and <em>connect to AI agent</em>, as well as a step for the creation of the service itself <em>spring.beans.instantiate</em>. As seen earlier, we can also monitor these startup steps in the Java Flight Recorder or programmatically via the <em>BufferingApplicationStartup</em>.</p>
<div>
<h2 id="bd-implementing-the-applicationstartup-interface" data-id="implementing-the-applicationstartup-interface">7. Implementing the <em>ApplicationStartup</em> Interface</h2>
<div class="bd-anchor" id="implementing-the-applicationstartup-interface"></div>
<p>The recommended way to access Spring startup metrics is using one of the three implementations of the <em>ApplicationStartup</em> interface that Spring provides. However, it&#8217;s possible to provide a custom implementation:</p>
<pre><code class="language-java">public class CustomStartup implements ApplicationStartup {
    @Override
    public StartupStep start(String name) {
        return new CustomStartupStep(name);
    }
}</code></pre>
<p>In this case, we should also provide an implementation of the <em>StartupStep</em> interface:</p>
<pre><code class="language-java">public class CustomStartupStep implements StartupStep {
    private String name;
    public CustomStartupStep(String name) {
        this.name = name;
    }
    @Override
    public String getName() {
        return name;
    }
    // Override other methods
}</code></pre>
</div>
<p>Since it&#8217;s a rare scenario, we don&#8217;t discuss a complete implementation of a custom <em>ApplicationStartup</em> class. <strong>One reason to implement a custom startup logic is the possibility to observe the steps in real time during context creation</strong>. This way, if needed, we can take action immediately, for example, to send a message if a bean creation takes a long time, instead of waiting until the entire context is created.</p>
<div>
<h2 id="bd-conclusion" data-id="conclusion">8. Conclusion</h2>
<div class="bd-anchor" id="conclusion"></div>
<p>In this article, we looked at how Spring tracks the creation of its application context through the <em>ApplicationStartup</em> interface.</p>
<p>Specifically, <strong>we covered the three implementations that Spring provides</strong>:</p>
<ul>
<li><em>DefaultApplicationStartup</em></li>
<li><em>BufferingApplicationStartup</em></li>
<li><em>FlightRecorderApplicationStartup</em></li>
</ul>
<p>In all cases, <strong>we learned how to access the recorded steps and add further steps to the timeline</strong>. Lastly, we had a quick look at how to plug a custom implementation of the <em>ApplicationStartup</em> interface.</p>
<p>As usual, the code for this article is available <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/spring-boot-3-observation">over on GitHub</a>.</p>
</div><p>The post <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-application-startup-tracking">Application Startup Tracking in Spring</a> first appeared on <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com">Baeldung</a>.</p><Img align="left" border="0" height="1" width="1" alt="" style="border:0;float:left;margin:0;padding:0;width:1px!important;height:1px!important;" hspace="0" src="https://feeds.feedblitz.com/~/i/963082187/0/baeldung">
<div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/963082187/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/963082187/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f11%2fSpring-Featured-Image-09-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/963082187/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/963082187/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/963082187/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/spring-application-startup-tracking#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/spring-application-startup-tracking/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</content:encoded>
					
					<wfw:commentRss>https://feeds.feedblitz.com/~/963082187/0/baeldung/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<webfeeds:featuredImage>https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Featured-Image-09-150x150.jpg</webfeeds:featuredImage></item>
<item>
<feedburner:origLink>https://www.baeldung.com/apache-causeway</feedburner:origLink>
		<title>Introduction to Apache Causeway</title>
		<link>https://feeds.feedblitz.com/~/963082190/0/baeldung</link>
					<comments>https://feeds.feedblitz.com/~/963082190/0/baeldung#respond</comments>
		
		<dc:creator><![CDATA[Francesco Galgani]]></dc:creator>
		<pubDate>Sat, 25 Jul 2026 19:11:34 +0000</pubDate>
				<category><![CDATA[Spring Boot]]></category>
		<category><![CDATA[JPA Entities]]></category>
		<category><![CDATA[popular]]></category>
		<category><![CDATA[REST Basics]]></category>
		<category><![CDATA[Spring Data JPA]]></category>
		<guid isPermaLink="false">https://www.baeldung.com/apache-causeway</guid>
					<description><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Boot-Featured-Image-02-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="max-width:100% !important;height:auto !important;float: left; margin-right: 5px;" loading="lazy" /><p>Learn about Apache Causeway by using it to build a domain-driven asset management application, including a generated UI, REST API, persistence, and business rules.</p>
<p>The post <a rel="NOFOLLOW" href="https://feeds.feedblitz.com/~/963082190/0/baeldung">Introduction to Apache Causeway</a> first appeared on <a rel="NOFOLLOW" href="https://www.baeldung.com">Baeldung</a>.</p><div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/963082190/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/963082190/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f11%2fSpring-Boot-Featured-Image-02-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/963082190/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/963082190/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/963082190/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/apache-causeway#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/apache-causeway/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</description>
										<content:encoded><![CDATA[<img src="https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Boot-Featured-Image-02-1024x536.jpg" class="webfeedsFeaturedVisual wp-post-image" alt="" style="float: left; margin-right: 5px;" decoding="async" loading="lazy" srcset="https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Boot-Featured-Image-02-1024x536.jpg 1024w, https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Boot-Featured-Image-02-300x157.jpg 300w, https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Boot-Featured-Image-02-768x402.jpg 768w, https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Boot-Featured-Image-02-100x52.jpg 100w, https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Boot-Featured-Image-02-600x314.jpg 600w, https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Boot-Featured-Image-02.jpg 1200w" sizes="auto, (max-width: 580px) 100vw, 580px" /><h2 id="bd-overview" data-id="overview">1. Overview</h2>
<div class="bd-anchor" id="overview"></div>
<p><a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://causeway.apache.org/"><strong>Apache Causeway</strong></a><strong> is a framework for building domain-driven applications on top of <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-boot-start">Spring Boot</a></strong>. Instead of beginning with controllers and pages, we describe domain objects, their state, behavior, and business rules. Causeway turns that description into a metamodel at runtime.</p>
<p>After that, the Wicket viewer and RESTful Objects viewer use the same metamodel to provide a web UI and a hypermedia API. This makes Causeway particularly useful for internal management software, where broad coverage of the domain and fast feedback often matter more than a custom interface.</p>
<p>In this tutorial, <strong>we&#8217;ll use Apache Causeway 3.6.0, Java 21, and Maven to build a small asset management application</strong>. Specifically, we&#8217;ll model assets such as laptops, monitors, and phones, add lifecycle actions and business rules, run the generated UI, and invoke one of the generated REST endpoints.</p>
<h2 id="bd-mental-model" data-id="mental-model">2. Mental Model</h2>
<div class="bd-anchor" id="mental-model"></div>
<p>The domain is the business area that the software addresses.</p>
<h3 id="bd-1-domain" data-id="1-domain"><strong>2.1. Domain</strong></h3>
<div class="bd-anchor" id="1-domain"></div>
<p><strong>The domain model is the set of types, relationships, operations, and rules that represent that area</strong>.</p>
<p>In this case, the domain is the management of company hardware:</p>
<ul>
<li>which assets exist</li>
<li>who has them</li>
<li>which lifecycle transitions are valid</li>
</ul>
<p>So, the main domain object here is <em>Asset</em>. Because instances are stored in a database, <em>Asset</em> is also a domain entity. A view model, by contrast, could represent temporary or calculated information without having a persistent identity. Causeway exposes scalar values such as <em>type</em>, <em>serialNumber</em>, <em>status</em>, and <em>assignedTo</em> as properties. A relationship to several other objects would be a collection.</p>
<p>Behavior appears as actions such as <em>assignTo()</em>, <em>returnToInventory()</em>, and <em>retire()</em>. Operations that don&#8217;t naturally belong to one asset, such as creating or finding assets, live in the <em>Assets</em> domain service. <strong>Causeway uses these domain members as the common vocabulary for both its UI and REST viewers</strong>.</p>
<h3 id="bd-2-causeway-vs-traditional-spring-mvc-application" data-id="2-causeway-vs-traditional-spring-mvc-application">2.2. Causeway vs. Traditional Spring MVC Application</h3>
<div class="bd-anchor" id="2-causeway-vs-traditional-spring-mvc-application"></div>
<p>In a <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/spring-mvc-tutorial">traditional Spring MVC application</a>, a request usually reaches a controller, which performs several actions:</p>
<ul>
<li>calls an application service</li>
<li>prepares a model or DTO</li>
<li>selects a view or produces a response</li>
</ul>
<p><strong>Even when the business logic is well isolated, each new use case generally needs explicit web-layer code</strong>.</p>
<p>Causeway changes the starting point. It introspects entities, domain services, annotations, and method signatures, and records them in its metamodel. The viewers render the properties, collections, actions, parameters, and validation messages that the metamodel describes. Therefore, <strong>we can add a basic use case without creating a matching controller, form, and template</strong>.</p>
<p>This doesn&#8217;t remove the rest of the application architecture. We still configure Spring modules, persistence, security, and menu layout, and we can place complex orchestration in application services when appropriate. <strong>Causeway reduces presentation-layer repetition by making the domain model drive the interaction</strong>.</p>
<h3 id="bd-3-when-causeway-is-a-good-fit" data-id="3-when-causeway-is-a-good-fit">2.3. When Causeway Is a Good Fit</h3>
<div class="bd-anchor" id="3-when-causeway-is-a-good-fit"></div>
<p>This model works well for back-office tools, administration systems, internal management applications, and domain-heavy prototypes. Such systems often combine structured data with many business operations, so a consistent generated interface lets a team validate terminology, workflows, and rules early.</p>
<p><strong>It&#8217;s a weaker fit when the primary requirement is a highly branded consumer experience, pixel-level interaction design, or a page flow that doesn&#8217;t map naturally to domain objects</strong>. In such cases, we can place a custom client in front of the Causeway APIs, but doing so sacrifices some of the development speed offered by the generic viewer.</p>
<p>The <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://causeway.apache.org/docs/3.6.0/what-is-apache-causeway/common-use-cases.html">common-use-case documentation</a> of the framework presents the same spectrum, from prototyping and generic line-of-business UIs to custom clients. <strong>The important decision is whether domain coverage or presentation control is the stronger requirement</strong>.</p>
<h2 id="bd-building-an-internal-asset-management-application" data-id="building-an-internal-asset-management-application">3. Building an Internal Asset Management Application</h2>
<div class="bd-anchor" id="building-an-internal-asset-management-application"></div>
<p>Let&#8217;s model an asset with several characteristics:</p>
<ul>
<li>type</li>
<li>serial number</li>
<li>status</li>
<li>assignee (optional)</li>
</ul>
<p>The normal asset lifecycle is <em>AVAILABLE</em> to <em>ASSIGNED</em>, then back to <em>AVAILABLE</em>, while an available asset can also become <em>RETIRED</em>.</p>
<p>A runnable Causeway application needs framework module imports, persistence, security, and menu configuration in addition to the central domain code. <strong>To follow along, let&#8217;s keep the <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/eugenp/tutorials/tree/master/apache-causeway">complete project on GitHub</a> open</strong>. The snippets that follow cover the decisions relevant to the tutorial, while the repository contains the supporting bootstrap and configuration files.</p>
<h3 id="bd-1-project-setup" data-id="1-project-setup">3.1. Project Setup</h3>
<div class="bd-anchor" id="1-project-setup"></div>
<p>To begin with, we need JDK 21 and Maven 3.9.11. <strong>The <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://central.sonatype.com/artifact/org.apache.causeway.app/causeway-app-starter-parent/3.6.0">Causeway application starter parent</a> manages the compatible Spring Boot and framework dependency versions</strong>. In <em>pom.xml</em>, let&#8217;s add the web application bundle, both viewers, Simple Security, JPA with EclipseLink, and an H2 in-memory database:</p>
<pre><code class="language-xml">&lt;project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"&gt;
    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;
    &lt;parent&gt;
        &lt;groupId&gt;org.apache.causeway.app&lt;/groupId&gt;
        &lt;artifactId&gt;causeway-app-starter-parent&lt;/artifactId&gt;
        &lt;version&gt;3.6.0&lt;/version&gt;
        &lt;relativePath/&gt;
    &lt;/parent&gt;
    &lt;groupId&gt;com.baeldung&lt;/groupId&gt;
    &lt;artifactId&gt;apache-causeway&lt;/artifactId&gt;
    &lt;version&gt;1.0.0-SNAPSHOT&lt;/version&gt;
    &lt;!-- The name and description are omitted. --&gt;
    &lt;dependencies&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.causeway.mavendeps&lt;/groupId&gt;
            &lt;artifactId&gt;causeway-mavendeps-webapp&lt;/artifactId&gt;
            &lt;type&gt;pom&lt;/type&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.causeway.viewer&lt;/groupId&gt;
            &lt;artifactId&gt;causeway-viewer-wicket-viewer&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.causeway.viewer&lt;/groupId&gt;
            &lt;artifactId&gt;causeway-viewer-restfulobjects-jaxrsresteasy&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.causeway.security&lt;/groupId&gt;
            &lt;artifactId&gt;causeway-security-simple&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.causeway.persistence&lt;/groupId&gt;
            &lt;artifactId&gt;causeway-persistence-jpa-eclipselink&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.causeway.viewer&lt;/groupId&gt;
            &lt;artifactId&gt;causeway-viewer-wicket-applib&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;com.h2database&lt;/groupId&gt;
            &lt;artifactId&gt;h2&lt;/artifactId&gt;
            &lt;scope&gt;runtime&lt;/scope&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework&lt;/groupId&gt;
            &lt;artifactId&gt;spring-instrument&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;!-- JUnit, Mockito, and Causeway test dependencies are omitted. --&gt;
    &lt;/dependencies&gt;
    &lt;!-- EclipseLink weaving and Spring Boot packaging plugins are omitted. --&gt;
    &lt;properties&gt;
        &lt;java.version&gt;21&lt;/java.version&gt;
        &lt;maven.compiler.release&gt;21&lt;/maven.compiler.release&gt;
    &lt;/properties&gt;
&lt;/project&gt;</code></pre>
<p>The web application bundle supplies the common runtime dependencies. The viewer artifacts add the generated Wicket interface and RESTful Objects API, while Simple Security and H2 keep this example self-contained.</p>
<p>Next, a small Spring configuration class identifies the application code:</p>
<pre><code class="language-java">@Configuration
@ComponentScan(basePackageClasses = Assets.class)
@EnableJpaRepositories(basePackageClasses = AssetRepository.class)
@EntityScan(basePackageClasses = Asset.class)
public class AssetManagementModule {
}</code></pre>
<p>Let&#8217;s break it down:</p>
<ul>
<li><em>@ComponentScan</em> discovers the domain service</li>
<li><em>@EnableJpaRepositories</em> enables the Spring Data repository</li>
<li><em>@EntityScan</em> registers the JPA entity</li>
</ul>
<p>Then, we import this configuration together with the required Causeway modules in <em>AppManifest</em>:</p>
<pre><code class="language-java">@Configuration
@Import({
    CausewayModuleApplibMixins.class,
    CausewayModuleCoreRuntimeServices.class,
    CausewayModuleSecuritySimple.class,
    CausewayModulePersistenceJpaEclipselink.class,
    CausewayModuleViewerRestfulObjectsJaxrsResteasy.class,
    CausewayModuleViewerWicketApplibMixins.class,
    CausewayModuleViewerWicketViewer.class,
    AssetManagementModule.class
})
@PropertySource(CausewayPresets.NoTranslations)
public class AppManifest {
    // Password encoding and the demo-only SimpleRealm configuration are omitted
}</code></pre>
<p>Importing a module makes its Spring beans and Causeway features available to the application.</p>
<p>The repository also configures H2, EclipseLink schema creation, the demo user, and the menu layout. In <em>application.yml</em>, we set <em>causeway.applib.annotation.action.explicit</em> to <em>true</em>, so only methods explicitly annotated with <em>@Action</em> become actions.</p>
<h3 id="bd-2-creating-the-asset-domain-entity" data-id="2-creating-the-asset-domain-entity">3.2. Creating the Asset Domain Entity</h3>
<div class="bd-anchor" id="2-creating-the-asset-domain-entity"></div>
<p>Let&#8217;s define the persistent type and give it a stable logical name:</p>
<pre><code class="language-java">@Entity
@Table(
    schema = "assets",
    name = "Asset",
    uniqueConstraints = @UniqueConstraint(
        name = "Asset__serialNumber__UNQ",
        columnNames = "serial_number"
    )
)
@EntityListeners(CausewayEntityListener.class)
@Named("assets.Asset")
@DomainObject
@DomainObjectLayout
public class Asset implements Comparable&lt;Asset&gt; {
    protected Asset() {
    }
    public Asset(final AssetType type, final String serialNumber) {
        this.type = type;
        this.serialNumber = serialNumber;
        this.status = AssetStatus.AVAILABLE;
    }
    // The identifier, version, comparison helpers, and members discussed below are omitted
}</code></pre>
<p>So, let&#8217;s take a closer look at what the code means:</p>
<ul>
<li>The JPA annotations map the entity and enforce a database-level uniqueness constraint on <em>serial_number</em>.</li>
<li><em>@Named</em> supplies the logical identifier that Causeway uses independently of the Java package name.</li>
<li><em>@DomainObject</em> explicitly identifies the class as a Causeway domain object.</li>
<li><em>CausewayEntityListener</em> connects JPA lifecycle events to Causeway, including service injection and lifecycle notifications.</li>
<li>The public constructor establishes the first lifecycle invariant: every new asset begins in the <em>AVAILABLE</em> status.</li>
</ul>
<p>JPA maps the stored values, while Causeway recognizes the corresponding getters as properties. To be clear, let&#8217;s see the identity properties:</p>
<pre><code class="language-java">@Enumerated(EnumType.STRING)
@Column(name = "type", nullable = false, length = 20)
private AssetType type;
@PropertyLayout(fieldSetId = LayoutConstants.FieldSetId.IDENTITY, sequence = "1")
public AssetType getType() {
    return type;
}
@Column(name = "serial_number", nullable = false, length = 80)
private String serialNumber;
@Title(prepend = "Asset: ")
@Property(maxLength = 80)
@PropertyLayout(fieldSetId = LayoutConstants.FieldSetId.IDENTITY, sequence = "2")
public String getSerialNumber() {
    return serialNumber;
}
// Status and assignedTo are omitted here to focus on the identity properties
</code></pre>
<p>There are several important details:</p>
<ul>
<li><em>EnumType.STRING</em> stores values such as <em>LAPTOP</em> instead of fragile ordinal numbers.</li>
<li><em>@PropertyLayout</em> groups and orders members in the generated UI.</li>
<li><em>@Title</em> makes the serial number part of the object&#8217;s display title.</li>
</ul>
<p>The remaining properties hold an <em>AssetStatus</em> and an optional employee name.</p>
<h3 id="bd-3-adding-domain-behavior-with-actions" data-id="3-adding-domain-behavior-with-actions">3.3. Adding Domain Behavior With Actions</h3>
<div class="bd-anchor" id="3-adding-domain-behavior-with-actions"></div>
<p>Notably, we don&#8217;t want callers to change lifecycle fields independently through setters. Instead, <strong>the entity exposes operations that update related state together</strong>.</p>
<p>First, the assignment action records an employee and changes the status:</p>
<pre><code class="language-java">@Action(semantics = SemanticsOf.IDEMPOTENT)
@ActionLayout(
    fieldSetId = LayoutConstants.FieldSetId.DETAILS,
    position = ActionLayout.Position.PANEL,
    describedAs = "Assigns an available asset to an employee"
)
public Asset assignTo(
    @Parameter(maxLength = 100)
    @ParameterLayout(named = "Employee") final String employee) {
    assignedTo = employee.trim();
    status = AssetStatus.ASSIGNED;
    return this;
}</code></pre>
<p>Causeway renders <em>assignTo()</em> as an action and derives its prompt from the parameter metadata. By returning <em>this</em>, it tells the viewer to continue with the updated asset. The <em>IDEMPOTENT</em> semantic describes the intended invocation semantics, but it doesn&#8217;t make the Java implementation idempotent automatically.</p>
<p>The other lifecycle actions return an asset to inventory or retire it:</p>
<pre><code class="language-java">@Action(semantics = SemanticsOf.IDEMPOTENT)
@ActionLayout(
    fieldSetId = LayoutConstants.FieldSetId.DETAILS,
    position = ActionLayout.Position.PANEL,
    describedAs = "Returns an assigned asset to inventory"
)
public Asset returnToInventory() {
    assignedTo = null;
    status = AssetStatus.AVAILABLE;
    return this;
}
@Action(semantics = SemanticsOf.IDEMPOTENT_ARE_YOU_SURE)
@ActionLayout(
    fieldSetId = LayoutConstants.FieldSetId.DETAILS,
    position = ActionLayout.Position.PANEL,
    describedAs = "Permanently retires an asset"
)
public Asset retire() {
    assignedTo = null;
    status = AssetStatus.RETIRED;
    return this;
}</code></pre>
<p>Specifically, <em>returnToInventory()</em> clears the assignee as it restores <em>AVAILABLE</em>. Retirement is deliberately explicit, and <em>IDEMPOTENT_ARE_YOU_SURE</em> asks the Wicket viewer for confirmation.</p>
<p>At this point the methods express the state changes, but we still need to control when each change is valid.</p>
<h3 id="bd-4-adding-business-rules" data-id="4-adding-business-rules">3.4. Adding Business Rules</h3>
<div class="bd-anchor" id="4-adding-business-rules"></div>
<p>Causeway associates supporting methods with domain members by naming convention. For example, <em>disableAssignTo()</em> controls the availability of <em>assignTo()</em>, while <em>validate0AssignTo()</em> validates parameter zero, which is the first parameter:</p>
<pre><code class="language-java">@MemberSupport
public String disableAssignTo() {
    return status == AssetStatus.AVAILABLE
        ? null
        : "Only available assets can be assigned";
}
@MemberSupport
public String validate0AssignTo(final String employee) {
    return employee == null || employee.isBlank()
        ? "Employee name is required"
        : null;
}
@MemberSupport
public String disableReturnToInventory() {
    return status == AssetStatus.ASSIGNED
        ? null
        : "Only assigned assets can be returned";
}
@MemberSupport
public String disableRetire() {
    if (status == AssetStatus.ASSIGNED) {
        return "Return the asset before retiring it";
    }
    return status == AssetStatus.RETIRED
        ? "The asset is already retired"
        : null;
}</code></pre>
<p>Here, <em>null</em> result permits the interaction. A message disables or rejects it and gives the viewer or API client a reason. Consequently, only an available asset can be assigned, only an assigned asset can be returned, and an assigned asset must be returned before retirement.</p>
<p><em>@MemberSupport</em> also lets Causeway validate that a supporting method still matches an existing domain member.</p>
<p><strong>String parameters are mandatory by default</strong>, so the framework rejects an empty <em>Employee</em> field before calling the action. The validator additionally handles whitespace-only input. These checks apply to Causeway-managed interactions; a direct Java call to <em>assignTo()</em> remains an ordinary method call and doesn&#8217;t invoke the supporting methods automatically.</p>
<p><strong>The business rules are part of the domain interaction, so both generated viewers can enforce them without duplicating conditions in a controller or page</strong>.</p>
<h3 id="bd-5-creating-an-asset-domain-service" data-id="5-creating-an-asset-domain-service">3.5. Creating an Asset Domain Service</h3>
<div class="bd-anchor" id="5-creating-an-asset-domain-service"></div>
<p>Entity actions operate on one existing asset. Creation and queries belong in a domain service backed by a <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/the-persistence-layer-with-spring-data-jpa">Spring Data JPA repository</a>:</p>
<pre><code class="language-java">@Named("assets.Assets")
@DomainService
@Priority(PriorityPrecedence.EARLY)
public class Assets {
    private final RepositoryService repositoryService;
    private final AssetRepository assetRepository;
    @Inject
    public Assets(
        final RepositoryService repositoryService,
        final AssetRepository assetRepository) {
        this.repositoryService = repositoryService;
        this.assetRepository = assetRepository;
    }
    @Action(semantics = SemanticsOf.NON_IDEMPOTENT)
    @ActionLayout(promptStyle = PromptStyle.DIALOG_MODAL)
    public Asset create(
        @ParameterLayout(named = "Type") final AssetType type,
        @Parameter(maxLength = 80)
        @ParameterLayout(named = "Serial number") final String serialNumber) {
        return repositoryService.persist(new Asset(type, serialNumber.trim()));
    }
    @MemberSupport
    public String validate1Create(final String serialNumber) {
        if (serialNumber == null || serialNumber.isBlank()) {
            return "Serial number is required";
        }
        return assetRepository.findBySerialNumberIgnoreCase(serialNumber.trim()).isPresent()
            ? "An asset with this serial number already exists"
            : null;
    }
    // listAll() appears in the API section; findBySerialNumber() is in the complete project
}</code></pre>
<p><em>@DomainService</em> includes the service in the metamodel, <em>@Named</em> assigns the logical name used by the REST viewer, and <em>@Priority</em> places it early in Spring&#8217;s ordering.</p>
<p><em>RepositoryService</em> persists a new entity through the Causeway abstraction, whereas <em>AssetRepository</em> provides application-specific queries.</p>
<p>The repository itself uses Spring Data-derived queries, so it doesn&#8217;t need an implementation class:</p>
<pre><code class="language-java">public interface AssetRepository extends JpaRepository&lt;Asset, Long&gt; {
    List&lt;Asset&gt; findAllByOrderBySerialNumberAsc();
    List&lt;Asset&gt; findBySerialNumberContainingIgnoreCaseOrderBySerialNumberAsc(
        String serialNumber
    );
    Optional&lt;Asset&gt; findBySerialNumberIgnoreCase(String serialNumber);
}</code></pre>
<p>The method names describe the ordering, partial match, and case-insensitive exact match that Spring Data generates. <strong>This keeps persistence queries separate from the Causeway-facing service actions</strong>.</p>
<p>The supporting method name <em>validate1Create()</em> refers to parameter one, the second parameter of <em>create()</em>. It rejects blank serial numbers and existing case-insensitive duplicates before persistence. The database constraint additionally protects against concurrent exact-value duplicates. Enforcing case-insensitive uniqueness at the database level would require normalization, a suitable index, or a case-insensitive collation.</p>
<p>Finally, <em>menubars.layout.xml</em> explicitly places the three service actions in the <em>Assets</em> menu:</p>
<pre><code class="language-xml">&lt;!-- The menuBars root, namespaces, primary container, and non-Assets menus are omitted. --&gt;
&lt;mb3:menu&gt;
    &lt;mb3:named&gt;Assets&lt;/mb3:named&gt;
    &lt;mb3:section&gt;
        &lt;mb3:serviceAction objectType="assets.Assets" id="create"/&gt;
        &lt;mb3:serviceAction objectType="assets.Assets" id="findBySerialNumber"/&gt;
        &lt;mb3:serviceAction objectType="assets.Assets" id="listAll"/&gt;
    &lt;/mb3:section&gt;
&lt;/mb3:menu&gt;</code></pre>
<p>The <em>objectType</em> value matches the logical name of the service, and each <em>id</em> matches an action method. <em>@DomainService</em> registers the service in the metamodel, each <em>@Action</em> exposes a method, and the layout file determines this menu grouping.</p>
<h3 id="bd-6-entry-point" data-id="6-entry-point">3.6. Entry Point</h3>
<div class="bd-anchor" id="6-entry-point"></div>
<p>The application entry point activates the Causeway prototyping preset and then starts Spring Boot:</p>
<pre><code class="language-java">@SpringBootApplication
@Import(AppManifest.class)
public class AssetManagementApplication extends SpringBootServletInitializer {
    public static void main(final String[] args) {
        CausewayPresets.prototyping();
        SpringApplication.run(AssetManagementApplication.class, args);
    }
}</code></pre>
<p>So, let&#8217;s run the application.</p>
<h2 id="bd-running-the-internal-asset-management-application" data-id="running-the-internal-asset-management-application">4. Running the Internal Asset Management Application</h2>
<div class="bd-anchor" id="running-the-internal-asset-management-application"></div>
<p><strong>From the <em>apache-causeway</em> module, we can build and run the application</strong>:</p>
<pre><code class="language-bash">mvn clean install
mvn spring-boot:run</code></pre>
<p>Next, let&#8217;s open <em>http://localhost:8080/wicket/</em> and sign in with the demo credentials <em>admin</em> and <em>pass</em>.</p>
<h3 id="bd-1-demo-interaction" data-id="1-demo-interaction">4.1. Demo Interaction</h3>
<div class="bd-anchor" id="1-demo-interaction"></div>
<p>From the <em>Assets</em> menu, let&#8217;s choose <em>Create</em>, select <em>Laptop</em>, and enter <em>LT-001</em>. Causeway immediately renders the new object using its title, properties, field sets, and actions:</p>
<p><a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/wp-content/uploads/2026/07/causeway-available-asset.png"><img decoding="async" class="alignnone size-full wp-image-256161" src="https://www.baeldung.com/wp-content/uploads/2026/07/causeway-available-asset.png" alt="An available asset with the actions derived from its domain model." /></a></p>
<p>When we open <em>Assign To</em> and submit an empty employee, the mandatory parameter prevents the invocation, and the generated prompt displays the validation error:</p>
<p><a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/wp-content/uploads/2026/07/causeway-assignment-validation.png"><img decoding="async" class="alignnone size-full wp-image-256166" src="https://www.baeldung.com/wp-content/uploads/2026/07/causeway-assignment-validation.png" alt="The generated action prompt rejects a missing employee." /></a></p>
<p>After entering <em>Alice</em>, the action changes the asset to <em>ASSIGNED</em>. The same page now enables <em>Return To Inventory</em> and disables <em>Assign To</em> and <em>Retire</em>, reflecting the supporting methods without page-specific conditional code:</p>
<p><a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/wp-content/uploads/2026/07/causeway-assigned-asset-rules.png"><img decoding="async" class="alignnone size-full wp-image-256173" src="https://www.baeldung.com/wp-content/uploads/2026/07/causeway-assigned-asset-rules.png" alt="Assigned asset - The generated UI reflects the asset's new state and available transitions." /></a></p>
<p><strong>No asset-specific controller, form, or HTML template was required for this generic UI</strong>.</p>
<p>The prototyping preset, in-memory H2 database, automatic schema creation, and <em>SimpleRealm</em> credentials are appropriate only for this local example. A production application should use durable persistence, schema migrations, production security, and a production-ready Causeway configuration.</p>
<h3 id="bd-2-generated-api" data-id="2-generated-api">4.2. Generated API</h3>
<div class="bd-anchor" id="2-generated-api"></div>
<p>The service marks its list operation as safe:</p>
<pre><code class="language-java">@Action(semantics = SemanticsOf.SAFE)
public List&lt;Asset&gt; listAll() {
    return assetRepository.findAllByOrderBySerialNumberAsc();
}</code></pre>
<p><strong>The <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://causeway.apache.org/vro/3.6.0/about.html">RESTful Objects viewer</a> maps this action to an authenticated <em>GET</em></strong>. With the application still running, we can invoke it using the demo account:</p>
<pre><code class="language-bash">$ curl -i -u admin:pass \
    -H 'Accept: application/json' \
    'http://localhost:8080/restful/services/assets.Assets/actions/listAll/invoke'</code></pre>
<p>The URL contains the logical service name <em>assets.Assets</em> and the action identifier <em>listAll</em>. The verified invocation returns <em>200 OK</em> and reaches the domain service rather than exposing <em>AssetRepository</em> directly.</p>
<p>The response isn&#8217;t a raw JSON array. It is a RESTful Objects <em>DomainObjectList</em> representation containing links, relation types, and media-type profiles that allow a client to navigate the domain.</p>
<p>This is useful for generic clients and integrations, but <strong>before treating it as a public, versioned API, we should deliberately restrict the exposed surface or define a client-specific representation</strong>.</p>
<h2 id="bd-conclusion" data-id="conclusion">5. Conclusion</h2>
<div class="bd-anchor" id="conclusion"></div>
<p>In this article, <strong>we began with the Causeway domain-first mental model and built an internal asset management application</strong>. Specifically, we mapped a JPA entity, expressed lifecycle behavior as actions, enforced valid transitions with supporting methods, and added a domain service for creation and queries.</p>
<p>After that, we ran the generated Wicket UI and invoked the RESTful Objects API. Causeway is a strong option when a behavior-rich domain and rapid delivery of consistent management screens outweigh the need for a fully custom interface.</p>
<p>The complete source code is available <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://github.com/eugenp/tutorials/tree/master/apache-causeway">over on GitHub</a>.</p><p>The post <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com/apache-causeway">Introduction to Apache Causeway</a> first appeared on <a href="https://feeds.feedblitz.com/~/t/0/0/baeldung/~https://www.baeldung.com">Baeldung</a>.</p><Img align="left" border="0" height="1" width="1" alt="" style="border:0;float:left;margin:0;padding:0;width:1px!important;height:1px!important;" hspace="0" src="https://feeds.feedblitz.com/~/i/963082190/0/baeldung">
<div style="clear:both;padding-top:0.2em;"><a href="https://feeds.feedblitz.com/_/28/963082190/baeldung"><img height="20" src="https://assets.feedblitz.com/i/fblike20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/29/963082190/baeldung,https%3a%2f%2fwww.baeldung.com%2fwp-content%2fuploads%2f2024%2f11%2fSpring-Boot-Featured-Image-02-1024x536.jpg"><img height="20" src="https://assets.feedblitz.com/i/pinterest20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/24/963082190/baeldung"><img height="20" src="https://assets.feedblitz.com/i/x.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/19/963082190/baeldung"><img height="20" src="https://assets.feedblitz.com/i/email20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a href="https://feeds.feedblitz.com/_/20/963082190/baeldung"><img height="20" src="https://assets.feedblitz.com/i/rss20.png" style="border:0;margin:0;padding:0;"></a>&#160;<a rel="NOFOLLOW" title="View Comments" href="https://www.baeldung.com/apache-causeway#respond"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/comments20.png"></a>&#160;<a title="Follow Comments via RSS" href="https://www.baeldung.com/apache-causeway/feed"><img height="20" style="border:0;margin:0;padding:0;" src="https://assets.feedblitz.com/i/commentsrss20.png"></a>&#160;</div>]]>
</content:encoded>
					
					<wfw:commentRss>https://feeds.feedblitz.com/~/963082190/0/baeldung/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<webfeeds:featuredImage>https://www.baeldung.com/wp-content/uploads/2024/11/Spring-Boot-Featured-Image-02-150x150.jpg</webfeeds:featuredImage></item>
</channel></rss>

