Sitemap

Spring AI Recipe: Composing ChatClient Behavior

3 min readApr 6, 2026

--

Press enter or click to view image in full size

In a previous recipe, ChatClientBuilderCustomizer was used to add conversational memory to a simple chat loop.

That same mechanism becomes even more useful as you begin adding tools, system prompts, and other behavior to a ChatClient.

This recipe demonstrates how to use ChatClientBuilderCustomizer to compose ChatClient behavior in a modular, maintainable way.

NOTE: This recipe has been updated since it’s original publication to reflect changes in Spring AI 2.0.0. Particularly, ChatClientCustomer has been changed to ChatClientBuilderCustomizer.

Defining a simple tool

Suppose you want to add a tool that retrieves weather information by ZIP code:

@Service
public class WeatherTools {
@Tool(name = "get-weather-for-zipcode",
description = "Get weather for a given zipcode")
public Weather getWeatherForZipcode(
@ToolParam(description = "The zipcode to get the weather for") String zipcode) {
return new Weather(zipcode, "Raining cats and dogs", "81.5F");
}
}

For completeness, here is the Weather record:

public record Weather(
String zipcode,
String conditions,
String temperature) {
}

The direct approach

One way to configure this tool is directly in the ChatClient bean:

@Bean
ChatClient chatClient(
ChatClient.Builder chatClientBuilder, WeatherTools weatherTools) {
return chatClientBuilder
.defaultTools(weatherTools)
.build();
}

While this works, it introduces additional parameters into the bean method and becomes harder to maintain as more features are added.

Composing behavior with ChatClientBuilderCustomizer

A cleaner approach is to use ChatClientBuilderCustomizer . For example, here are two such ChatClientBuilderCustomizer bean methods:

@Bean
ChatClientBuilderCustomizer defaultTools(WeatherTools tools) {
return builder -> builder.defaultTools(tools);
}

@Bean
ChatClientBuilderCustomizer pirateTalk() {
return builder -> builder.defaultSystem("Talk like a pirate");
}

Each customizer is responsible for a single aspect of behavior:

  • One adds tools
  • One configures the system prompt

The base ChatClient bean can remain simple and focused.

Trying it out

If you are using the chat loop from the previous recipe, you can interact with it like this:

> What is the weather in Jal, New Mexico?

- Arrr! In Jal, New Mexico (ZIP 88252) it be rainin' cats an' dogs...

The response:

  • Uses the weather tool
  • Applies the pirate-style system message

Why this matters

As you begin adding more capabilities to ChatClient, a single configuration method can quickly become difficult to manage.

Using ChatClientBuilderCustomizer provides:

  • Separation of concerns
  • Composability
  • Improved readability
  • Easier extension and reuse

Each behavior can be added independently without cluttering the core configuration.

Conditional configuration

Another advantage of this approach is the ability to enable or disable features via configuration. First, annotate the ChatClientBuilderCustomizer beans with @ConditionalOnProperty :

@Bean
@ConditionalOnProperty("chatclient.tools.weather.enabled")
ChatClientBuilderCustomizer defaultTools(WeatherTools tools) {
return builder -> builder.defaultTools(tools);
}

@Bean
@ConditionalOnProperty("chatclient.pirate.enabled")
ChatClientBuilderCustomizer pirateTalk() {
return builder -> builder.defaultSystem("Talk like a pirate");
}

Then enable or disable them in configuration (e.g., application.properties or similar):

chatclient.tools.weather.enabled=true
chatclient.pirate.enabled=false

This allows you to toggle capabilities using:

  • application properties
  • environment variables
  • system properties
  • configuration servers

Takeaway

ChatClientBuilderCustomizer is more than a convenience—it’s a pattern for composing ChatClient behavior in a structured and maintainable way.

As your applications evolve, this approach makes it much easier to introduce new capabilities and manage complexity — especially in more advanced, agentic scenarios. Watch for me to use ChatClientBuilderCustomizer in future recipes.

Get the code for this recipe (and all of my other Spring AI recipes) at https://github.com/habuma/spring-ai-recipes.

--

--

Craig Walls
Craig Walls

Written by Craig Walls

Author Spring AI in Action, Spring in Action, and Build Talking Apps for Alexa