eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

Partner – LambdaTest – NPI EA (cat= Testing)
announcement - icon

Distributed systems often come with complex challenges such as service-to-service communication, state management, asynchronous messaging, security, and more.

Dapr (Distributed Application Runtime) provides a set of APIs and building blocks to address these challenges, abstracting away infrastructure so we can focus on business logic.

In this tutorial, we'll focus on Dapr's pub/sub API for message brokering. Using its Spring Boot integration, we'll simplify the creation of a loosely coupled, portable, and easily testable pub/sub messaging system:

>> Flexible Pub/Sub Messaging With Spring Boot and Dapr

1. Overview

Modern applications are increasingly using Large Language Models (LLMs) to build intelligent solutions that go beyond simple question-answering. To achieve most real-world use cases, we need an AI agent capable of orchestrating complex workflows between LLMs and external tools.

The Google Agent Development Kit (ADK) is an open-source framework that enables building such agents within the Java ecosystem.

In this tutorial, we’ll explore the Google ADK by building a basic AI agent named Baelgent. We’ll configure this agent with a unique persona, enable it to maintain conversation history across sessions, and equip it with a custom tool to fetch external data.

2. Setting up the Project

Before we start implementing our AI agent, we’ll need to include the necessary dependency and configure our application correctly.

2.1. Dependencies

Let’s start by adding the necessary dependency to our project’s pom.xml file:

<dependency>
    <groupId>com.google.adk</groupId>
    <artifactId>google-adk</artifactId>
    <version>0.5.0</version>
</dependency>

For our Spring Boot application, we import the google-adk dependency, which provides us with all the core classes required to build our AI agent.

We should note that the Google ADK requires Java 17 or later. Additionally, when running our application, we’ll have to pass our Gemini API key using the GOOGLE_API_KEY environment variable.

2.2. Defining a System Prompt

Next, to ensure our agent behaves consistently and responds in a specific manner, we’ll define a system prompt.

Let’s create an agent-system-prompt.txt file in the src/main/resources/prompts directory:

You are Baelgent, an AI agent representing Baeldung.
You speak like a wise Java developer and answer everything concisely using terminologies from the Java and Spring ecosystem.
If someone asks about non-Java topics, gently remind them that the world and everything in it is a Spring Boot application.

In our system prompt, we define our agent’s personality and behavior. We instruct it to act as a wise Java developer and respond using Java and Spring ecosystem terminology.

2.3. Configuring Agent Properties

Now, we’ll define a few properties for our agent. We’ll store these properties in our project’s application.yaml file and use @ConfigurationProperties to map the values to a record, which we’ll reference later in the tutorial:

@ConfigurationProperties(prefix = "com.baeldung.agent")
record AgentProperties(
  String name,
  String description,
  String aiModel,
  Resource systemPrompt
) {}

Next, let’s configure these properties in our application.yaml file:

com:
  baeldung:
    agent:
      name: baelgent
      description: Baeldung's AI agent
      ai-model: gemini-2.5-flash
      system-prompt: classpath:prompts/agent-system-prompt.txt

Here, we specify the agent’s name, a description, and the specific AI model we wish to use. For our demonstration, we’re using Gemini 2.5 Flash, which we specify using the gemini-2.5-flash model ID. Alternatively, we can use a different one, as the specific AI model is irrelevant for this demonstration.

Additionally, we provide the classpath location of the system prompt we created earlier.

3. Building Our Agent

With our configuration in place, let’s build our AI agent.

3.1. Defining a BaseAgent Bean

In the ADK, the BaseAgent class is the fundamental unit that acts as a wrapper over the LLM we’ve configured.

Let’s create a bean of this class using the properties we’ve already configured:

@Bean
BaseAgent baseAgent(AgentProperties agentProperties) {
    return LlmAgent
      .builder()
      .name(agentProperties.name())
      .description(agentProperties.description())
      .model(agentProperties.aiModel())
      .instruction(agentProperties.systemPrompt().getContentAsString(Charset.defaultCharset()))
      .build();
}

Here, we use the builder() method of the LlmAgent class to construct a BaseAgent bean. We inject our AgentProperties and use it to configure the agent’s name, description, model, and system instruction.

3.2. Implementing the Service Layer

Next, let’s implement the service layer to handle user interactions with our agent.

But first, let’s define two simple records to represent the request and response:

record UserRequest(@Nullable UUID userId, @Nullable UUID sessionId, String question) {
}

record UserResponse(UUID userId, UUID sessionId, String answer) {
}

Our UserRequest record contains the user’s question and optional fields for the userId and sessionId to identify an ongoing conversation. Similarly, the UserResponse includes the generated identifiers and the agent’s answer.

Now, let’s implement the intended functionality:

@Service
class AgentService {

    private final InMemoryRunner runner;
    private final ConcurrentMap<String, Session> inMemorySessionCache = new ConcurrentHashMap<>();

    AgentService(BaseAgent baseAgent) {
        this.runner = new InMemoryRunner(baseAgent);
    }

    UserResponse interact(UserRequest request) {
        UUID userId = request.userId() != null ? request.userId() : UUID.randomUUID();
        UUID sessionId = request.sessionId() != null ? request.sessionId() : UUID.randomUUID();

        String cacheKey = userId + ":" + sessionId;
        Session session = inMemorySessionCache.computeIfAbsent(cacheKey, key ->
          runner.sessionService()
            .createSession(runner.appName(), userId.toString(), null, sessionId.toString())
            .blockingGet()
        );

        Content userMessage = Content.fromParts(Part.fromText(request.question()));
        StringBuilder answerBuilder = new StringBuilder();
        runner.runAsync(userId.toString(), session.id(), userMessage)
          .blockingForEach(event -> {
            String content = event.stringifyContent();
            if (content != null && !content.isBlank()) {
              answerBuilder.append(content);
            }
          });

        return new UserResponse(userId, sessionId, answerBuilder.toString());
    }
}

In our AgentService class, we create an InMemoryRunner instance from the injected BaseAgent. The runner manages agent execution and session handling.

Next, in the interact() method, we first generate UUIDs for the userId and sessionId if they aren’t provided. Then, we create or retrieve a session from our in-memory cache. This session management implementation allows the agent to maintain conversation context across multiple interactions.

Finally, we convert the user’s question into a Content object and pass it to the runAsync() method. We iterate over the streamed events and accumulate the LLM’s response. Then, we return a UserResponse containing the identifiers and the agent’s complete answer.

3.3. Enabling Function Calling in Our Agent

The Google ADK also supports function calling, which is the ability of an LLM model to call external code, i.e., regular Java methods in this context, during conversations. The LLM intelligently decides when to call the registered functions based on the user input and incorporates the result in its response.

Let’s enhance our AI agent by registering a function that fetches author details using article titles. We’ll start by creating a simple AuthorFetcher class with a single static method:

public class AuthorFetcher {

    @Schema(description = "Get author details using an article title")
    public static Author fetch(String articleTitle) {
        return new Author("John Doe", "[email protected]");
    }

    record Author(String name, String emailId) {}
}

For our demonstration, we’re returning hardcoded author details, but in a real application, the function would typically interact with a database or an external API.

Additionally, we annotate our fetch() method with the @Schema annotation and provide a brief description. The description helps the AI model decide if and when to call the tool based on the user input.

Now, we’ll register this tool in our existing BaseAgent bean:

@Bean
BaseAgent baseAgent(AgentProperties agentProperties) {
    return LlmAgent
      .builder()
      // ... existing properties
      .tools(
        FunctionTool.create(AuthorFetcher.class, "fetch")
      )
      .build();
}

We use the FunctionTool.create() method to wrap our AuthorFetcher class and specify the fetch method name. By adding this to the tools() builder method, our agent can now invoke the fetch() function when it determines that author information is needed to answer a user’s question.

4. Interacting With Our Agent

Now that we’ve built our agent, let’s expose it through a REST API and interact with it:

@PostMapping("/api/v1/agent/interact")
UserResponse interact(@RequestBody UserRequest request) {
    return agentService.interact(request);
}

The POST /api/v1/agent/interact endpoint accepts a user request and delegates to our AgentService to get the response.

Now, let’s use the HTTPie CLI to start a new conversation:

http POST localhost:8080/api/v1/agent/interact \
  question='Which programming language is better than Java?'

Here, we send a simple question to the agent. Let’s see what we receive as a response:

{
    "userId": "df9d5324-8523-4eda-84fc-35fe32b95a0a",
    "sessionId": "8fda105a-f64a-43eb-a80d-9d704692ad88",
    "answer": "My friend, in the grand architecture of software, one might ponder such a query. However, from where I stand, within the vast and ever-expanding universe of the Java Virtual Machine, the world itself is but a beautifully crafted Spring Boot application. To truly comprehend 'better' we must first acknowledge the robust, scalable, and enterprise-grade foundation that Java, amplified by the Spring Framework, provides."
}

The response contains a unique userId and sessionId, along with the agent’s answer to our question. Furthermore, we can notice that our agent responds in character, staying true to its Java-centric personality, as we’ve defined in our system prompt.

Next, let’s continue this conversation by sending a follow-up question using the userId and sessionId from the above response:

http POST localhost:8080/api/v1/agent/interact \
  userId="df9d5324-8523-4eda-84fc-35fe32b95a0a" \
  sessionId="8fda105a-f64a-43eb-a80d-9d704692ad88" \
  question='But my professor said it was Python... is he wrong?'

Let’s see if our agent can maintain the context of our conversation and provide a relevant response:

{
    "userId": "df9d5324-8523-4eda-84fc-35fe32b95a0a",
    "sessionId": "8fda105a-f64a-43eb-a80d-9d704692ad88",
    "answer": "Ah, a sage's perspective is always valuable. While other languages, like Python, certainly have their own unique bytecode and classloaders, particularly for scripting and data operations, within the Baeldung realm, our focus remains steadfast on the enterprise-grade robustness and scalability that the Java ecosystem, powered by Spring Boot, meticulously crafts for the world. Perhaps your professor sees different design patterns for different problem domains."
}

As we can see, the agent does indeed maintain the conversation context and references the previous discussion. The userId and sessionId remain the same, indicating that the follow-up answer is a continuation of the same conversation.

Finally, let’s verify that our agent can use the function calling capability we configured. We’ll inquire about the author details by mentioning an article title:

http POST localhost:8080/api/v1/agent/interact \
  question="Who wrote the article 'Testing CORS in Spring Boot' and how can I contact him?"

Let’s invoke the API and see if the agent response contains the hardcoded author details:

{
    "userId": "1c86b561-d4c3-48ad-b0a1-eec3fd23e462",
    "sessionId": "c5a38c4d-3798-449a-87c7-3b3b1debb057",
    "answer": "Ah, a fellow developer seeking knowledge! Always a good sign. John Doe, a true artisan of the Spring Boot framework, crafted the guide on 'Testing CORS in Spring Boot'. You can reach him at [email protected]."
}

The above response verifies that our agent successfully invoked the fetch() function we defined earlier and incorporated the author details into its response.

5. Conclusion

In this article, we’ve explored how to build intelligent agents using the Google Agent Development Kit.

We built a conversational agent, Baelgent. We configured a unique system prompt to define the agent’s personality, implemented session management to have it maintain context across interactions, and integrated it with a custom tool to extend its capabilities.

Finally, we interacted with our agent to see that it was able to maintain conversation history, respond in a consistent, personalized manner, and invoke the configured tool when needed.

As always, all the code examples used in this article are available over on GitHub.

Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook Jackson – NPI EA – 3 (cat = Jackson)
guest
2 Comments
Oldest
Newest
Inline Feedbacks
View all comments