Let's get started with a Microservice Architecture with Spring Cloud:
Building AI Agents Using Google Agent Development Kit (ADK)
Last updated: January 28, 2026
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.















