Function Calling with Spring AI: Real Use Case Guide
A detailed step-by-step guide to implement Spring AI function calling, also called tool calling, using a real customer support order assistant example.
Function calling is now commonly called tool calling in Spring AI.
The idea is simple:
The AI model can decide that it needs a tool, but your Spring Boot application executes the tool.
The model does not directly access your database, payment system, email system, or internal APIs. It only asks your application to call an approved tool with structured arguments.
In this guide, we will build a real customer support assistant that can:
- Answer normal support questions.
- Check order status.
- Check whether an order can be cancelled.
- Cancel an order when allowed.
- Create a support ticket.
This is a practical use case because normal chat alone cannot know live order data or perform business actions.
Final APIs
| API | Method | Purpose |
|---|---|---|
/api/support/health |
GET |
Check service health |
/api/support/chat |
POST |
Ask the assistant a question |
/api/support/orders |
GET |
View sample in-memory orders |
/api/support/tickets |
GET |
View support tickets created by tool calls |
How Tool Calling Works
sequenceDiagram
participant User
participant App as Spring Boot App
participant LLM as AI Model
participant Tool as Java Tool
participant Data as Order/Ticket Data
User->>App: "Where is order ORD-1001?"
App->>LLM: User message + available tool schemas
LLM-->>App: Tool call request: getOrderStatus(orderId)
App->>Tool: Execute getOrderStatus("ORD-1001")
Tool->>Data: Read order data
Data-->>Tool: Order status
Tool-->>App: Tool result
App->>LLM: Tool result
LLM-->>App: Final user-friendly answer
App-->>User: "Order ORD-1001 has shipped..."
The model chooses when to call the tool. Your Java method controls what actually happens.
Tool Calling vs Normal Chat
| Normal Chat | Tool Calling |
|---|---|
| Model answers from prompt and training knowledge | Model can request approved Java methods |
| Cannot access live order status | Can call getOrderStatus(...) |
| Cannot create support tickets | Can call createSupportTicket(...) |
| Cannot safely perform business actions | Spring app validates and executes actions |
| Good for explanations | Good for live data and workflows |
Real Use Case: Order Support Assistant
Imagine a customer asks:
Where is my order ORD-1001?
The model does not know your order database. With tool calling, it can call:
getOrderStatus("ORD-1001")
Then the model can answer:
Order ORD-1001 has shipped and is expected to arrive on June 25, 2026.
Now imagine:
Cancel order ORD-1002.
The model can call:
cancelOrder("ORD-1002", "Customer requested cancellation")
But your Java method decides whether cancellation is allowed.
Tools and Frameworks
| Tool | Version | Purpose |
|---|---|---|
| Java | 21 or later | Application runtime |
| Spring Boot | 4.0.x | REST API framework |
| Spring AI | 2.0.0 | ChatClient and tool calling |
| OpenAI | Current API | Chat model with tool support |
| Maven | 3.9+ | Build tool |
| curl or Postman | Any current version | API testing |
Spring AI 2.0.x supports Spring Boot 4.0.x and 4.1.x. If your project is on Spring Boot 3.x, use the matching Spring AI 1.x version and documentation.
Project Structure
spring-ai-function-calling/
├── pom.xml
└── src/
└── main/
├── java/
│ └── com/
│ └── codewithvenu/
│ └── functioncalling/
│ ├── FunctionCallingApplication.java
│ ├── controller/
│ │ └── SupportAssistantController.java
│ ├── dto/
│ │ ├── ChatRequest.java
│ │ ├── ChatResponse.java
│ │ ├── OrderDto.java
│ │ └── TicketDto.java
│ ├── exception/
│ │ └── GlobalExceptionHandler.java
│ ├── model/
│ │ ├── Order.java
│ │ ├── OrderStatus.java
│ │ ├── SupportTicket.java
│ │ └── TicketPriority.java
│ ├── repository/
│ │ └── DemoSupportRepository.java
│ ├── service/
│ │ └── SupportAssistantService.java
│ └── tools/
│ └── OrderSupportTools.java
└── resources/
└── application.yml
Step 1: Create Maven Project
File: pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.0</version>
<relativePath/>
</parent>
<groupId>com.codewithvenu</groupId>
<artifactId>spring-ai-function-calling</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-ai-function-calling</name>
<description>Function calling with Spring AI</description>
<properties>
<java.version>21</java.version>
<spring-ai.version>2.0.0</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Step 2: Configure OpenAI
File: src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: spring-ai-function-calling
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4.1-mini
temperature: 0.2
Set your API key:
export OPENAI_API_KEY="your-openai-api-key-here"
Windows PowerShell:
$env:OPENAI_API_KEY="your-openai-api-key-here"
Step 3: Main Application Class
File: src/main/java/com/codewithvenu/functioncalling/FunctionCallingApplication.java
package com.codewithvenu.functioncalling;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class FunctionCallingApplication {
public static void main(String[] args) {
SpringApplication.run(FunctionCallingApplication.class, args);
}
}
Step 4: Create Domain Models
OrderStatus
File: src/main/java/com/codewithvenu/functioncalling/model/OrderStatus.java
package com.codewithvenu.functioncalling.model;
public enum OrderStatus {
PLACED,
PROCESSING,
SHIPPED,
DELIVERED,
CANCELLED
}
TicketPriority
File: src/main/java/com/codewithvenu/functioncalling/model/TicketPriority.java
package com.codewithvenu.functioncalling.model;
public enum TicketPriority {
LOW,
MEDIUM,
HIGH
}
Order
File: src/main/java/com/codewithvenu/functioncalling/model/Order.java
package com.codewithvenu.functioncalling.model;
import java.math.BigDecimal;
import java.time.LocalDate;
public record Order(
String orderId,
String customerEmail,
String itemName,
OrderStatus status,
BigDecimal amount,
LocalDate estimatedDeliveryDate,
boolean cancellable
) {
public Order cancel() {
return new Order(
orderId,
customerEmail,
itemName,
OrderStatus.CANCELLED,
amount,
estimatedDeliveryDate,
false
);
}
}
SupportTicket
File: src/main/java/com/codewithvenu/functioncalling/model/SupportTicket.java
package com.codewithvenu.functioncalling.model;
import java.time.Instant;
public record SupportTicket(
String ticketId,
String customerEmail,
String orderId,
String issue,
TicketPriority priority,
Instant createdAt
) {
}
Step 5: Create DTOs
ChatRequest
File: src/main/java/com/codewithvenu/functioncalling/dto/ChatRequest.java
package com.codewithvenu.functioncalling.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record ChatRequest(
@NotBlank(message = "message is required")
@Size(max = 4000, message = "message must be less than 4000 characters")
String message
) {
}
ChatResponse
File: src/main/java/com/codewithvenu/functioncalling/dto/ChatResponse.java
package com.codewithvenu.functioncalling.dto;
import java.time.Instant;
public record ChatResponse(
String answer,
Instant createdAt
) {
public static ChatResponse of(String answer) {
return new ChatResponse(answer, Instant.now());
}
}
OrderDto
File: src/main/java/com/codewithvenu/functioncalling/dto/OrderDto.java
package com.codewithvenu.functioncalling.dto;
import com.codewithvenu.functioncalling.model.Order;
import java.math.BigDecimal;
import java.time.LocalDate;
public record OrderDto(
String orderId,
String customerEmail,
String itemName,
String status,
BigDecimal amount,
LocalDate estimatedDeliveryDate,
boolean cancellable
) {
public static OrderDto from(Order order) {
return new OrderDto(
order.orderId(),
order.customerEmail(),
order.itemName(),
order.status().name(),
order.amount(),
order.estimatedDeliveryDate(),
order.cancellable()
);
}
}
TicketDto
File: src/main/java/com/codewithvenu/functioncalling/dto/TicketDto.java
package com.codewithvenu.functioncalling.dto;
import com.codewithvenu.functioncalling.model.SupportTicket;
import java.time.Instant;
public record TicketDto(
String ticketId,
String customerEmail,
String orderId,
String issue,
String priority,
Instant createdAt
) {
public static TicketDto from(SupportTicket ticket) {
return new TicketDto(
ticket.ticketId(),
ticket.customerEmail(),
ticket.orderId(),
ticket.issue(),
ticket.priority().name(),
ticket.createdAt()
);
}
}
Step 6: Create Demo Repository
For this tutorial, we use an in-memory repository. In a real project, this would call a database, external order service, payment service, or ticketing system.
File: src/main/java/com/codewithvenu/functioncalling/repository/DemoSupportRepository.java
package com.codewithvenu.functioncalling.repository;
import com.codewithvenu.functioncalling.model.Order;
import com.codewithvenu.functioncalling.model.OrderStatus;
import com.codewithvenu.functioncalling.model.SupportTicket;
import com.codewithvenu.functioncalling.model.TicketPriority;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Repository
public class DemoSupportRepository {
private final Map<String, Order> orders = new ConcurrentHashMap<>();
private final Map<String, SupportTicket> tickets = new ConcurrentHashMap<>();
public DemoSupportRepository() {
saveOrder(new Order(
"ORD-1001",
"[email protected]",
"Spring AI Course",
OrderStatus.SHIPPED,
new BigDecimal("49.99"),
LocalDate.now().plusDays(2),
false
));
saveOrder(new Order(
"ORD-1002",
"[email protected]",
"Java Interview Guide",
OrderStatus.PROCESSING,
new BigDecimal("19.99"),
LocalDate.now().plusDays(5),
true
));
saveOrder(new Order(
"ORD-1003",
"[email protected]",
"System Design Notes",
OrderStatus.DELIVERED,
new BigDecimal("29.99"),
LocalDate.now().minusDays(1),
false
));
}
public Optional<Order> findOrder(String orderId) {
return Optional.ofNullable(orders.get(orderId));
}
public List<Order> findAllOrders() {
return new ArrayList<>(orders.values());
}
public Order saveOrder(Order order) {
orders.put(order.orderId(), order);
return order;
}
public SupportTicket createTicket(String customerEmail, String orderId, String issue, TicketPriority priority) {
String ticketId = "TICKET-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
SupportTicket ticket = new SupportTicket(
ticketId,
customerEmail,
orderId,
issue,
priority,
Instant.now()
);
tickets.put(ticketId, ticket);
return ticket;
}
public List<SupportTicket> findAllTickets() {
return new ArrayList<>(tickets.values());
}
}
Step 7: Create Tool Class
This is the most important part.
Each @Tool method becomes available to the model. The model sees the tool name, description, and parameter schema. When needed, it asks Spring AI to execute the tool.
File: src/main/java/com/codewithvenu/functioncalling/tools/OrderSupportTools.java
package com.codewithvenu.functioncalling.tools;
import com.codewithvenu.functioncalling.dto.OrderDto;
import com.codewithvenu.functioncalling.dto.TicketDto;
import com.codewithvenu.functioncalling.model.Order;
import com.codewithvenu.functioncalling.model.SupportTicket;
import com.codewithvenu.functioncalling.model.TicketPriority;
import com.codewithvenu.functioncalling.repository.DemoSupportRepository;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
@Component
public class OrderSupportTools {
private final DemoSupportRepository repository;
public OrderSupportTools(DemoSupportRepository repository) {
this.repository = repository;
}
@Tool(description = "Get current order status, item details, estimated delivery date, amount, and cancellation eligibility for a given order ID")
public OrderDto getOrderStatus(
@ToolParam(description = "Order ID such as ORD-1001") String orderId
) {
Order order = repository.findOrder(orderId)
.orElseThrow(() -> new IllegalArgumentException("Order not found: " + orderId));
return OrderDto.from(order);
}
@Tool(description = "Cancel an order when the order is still cancellable. Use this only when the user clearly asks to cancel an order.")
public String cancelOrder(
@ToolParam(description = "Order ID such as ORD-1002") String orderId,
@ToolParam(description = "Short reason for cancellation") String reason
) {
Order order = repository.findOrder(orderId)
.orElseThrow(() -> new IllegalArgumentException("Order not found: " + orderId));
if (!order.cancellable()) {
return "Order " + orderId + " cannot be cancelled because its current status is " + order.status() + ".";
}
repository.saveOrder(order.cancel());
return "Order " + orderId + " has been cancelled. Reason: " + reason;
}
@Tool(description = "Create a customer support ticket for an order issue. Use this when the user has a complaint, delivery issue, refund request, damaged item, or needs human help.")
public TicketDto createSupportTicket(
@ToolParam(description = "Customer email address") String customerEmail,
@ToolParam(description = "Order ID related to the issue") String orderId,
@ToolParam(description = "Short description of the customer issue") String issue,
@ToolParam(description = "Ticket priority. Allowed values: LOW, MEDIUM, HIGH") TicketPriority priority
) {
SupportTicket ticket = repository.createTicket(customerEmail, orderId, issue, priority);
return TicketDto.from(ticket);
}
}
Why Tool Descriptions Matter
This description:
@Tool(description = "Cancel an order when the order is still cancellable. Use this only when the user clearly asks to cancel an order.")
is not decoration. The model uses it to decide:
- When to call the tool.
- What the tool does.
- Which arguments to provide.
- When not to call the tool.
Poor descriptions cause poor tool usage.
Step 8: Create Assistant Service
File: src/main/java/com/codewithvenu/functioncalling/service/SupportAssistantService.java
package com.codewithvenu.functioncalling.service;
import com.codewithvenu.functioncalling.dto.ChatResponse;
import com.codewithvenu.functioncalling.tools.OrderSupportTools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class SupportAssistantService {
private final ChatClient chatClient;
private final OrderSupportTools orderSupportTools;
public SupportAssistantService(ChatClient.Builder chatClientBuilder, OrderSupportTools orderSupportTools) {
this.orderSupportTools = orderSupportTools;
this.chatClient = chatClientBuilder
.defaultSystem("""
You are a customer support assistant for CodeWithVenu Store.
You can answer general support questions.
For live order status, cancellation, or ticket creation, use the provided tools.
Rules:
- Ask for an order ID if the user wants order information but did not provide one.
- Ask for customer email before creating a support ticket if it is missing.
- Do not cancel an order unless the user clearly asks to cancel it.
- If a tool says an order cannot be cancelled, explain the reason politely.
- Keep answers concise and helpful.
""")
.build();
}
public ChatResponse chat(String message) {
String answer = chatClient
.prompt()
.user(message)
.tools(orderSupportTools)
.call()
.content();
return ChatResponse.of(answer);
}
}
Important:
.tools(orderSupportTools)makes the@Toolmethods available for this request.- The model may call zero, one, or multiple tools.
- Spring AI handles the loop: model requests tool, app executes tool, result returns to model, model writes final answer.
Step 9: Create Controller
File: src/main/java/com/codewithvenu/functioncalling/controller/SupportAssistantController.java
package com.codewithvenu.functioncalling.controller;
import com.codewithvenu.functioncalling.dto.ChatRequest;
import com.codewithvenu.functioncalling.dto.ChatResponse;
import com.codewithvenu.functioncalling.dto.OrderDto;
import com.codewithvenu.functioncalling.dto.TicketDto;
import com.codewithvenu.functioncalling.repository.DemoSupportRepository;
import com.codewithvenu.functioncalling.service.SupportAssistantService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/support")
public class SupportAssistantController {
private final SupportAssistantService assistantService;
private final DemoSupportRepository repository;
public SupportAssistantController(SupportAssistantService assistantService, DemoSupportRepository repository) {
this.assistantService = assistantService;
this.repository = repository;
}
@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "UP", "service", "function-calling-support-assistant");
}
@PostMapping("/chat")
public ChatResponse chat(@Valid @RequestBody ChatRequest request) {
return assistantService.chat(request.message());
}
@GetMapping("/orders")
public List<OrderDto> orders() {
return repository.findAllOrders()
.stream()
.map(OrderDto::from)
.toList();
}
@GetMapping("/tickets")
public List<TicketDto> tickets() {
return repository.findAllTickets()
.stream()
.map(TicketDto::from)
.toList();
}
}
Step 10: Add Error Handling
File: src/main/java/com/codewithvenu/functioncalling/exception/GlobalExceptionHandler.java
package com.codewithvenu.functioncalling.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> fields = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
fields.put(error.getField(), error.getDefaultMessage())
);
Map<String, Object> body = new HashMap<>();
body.put("timestamp", Instant.now());
body.put("status", HttpStatus.BAD_REQUEST.value());
body.put("error", "Validation failed");
body.put("fields", fields);
return ResponseEntity.badRequest().body(body);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, Object>> handleBadRequest(IllegalArgumentException ex) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", Instant.now());
body.put("status", HttpStatus.BAD_REQUEST.value());
body.put("error", "Bad request");
body.put("message", ex.getMessage());
return ResponseEntity.badRequest().body(body);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleException(Exception ex) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", Instant.now());
body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
body.put("error", "Assistant request failed");
body.put("message", ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
}
}
In production, do not expose raw exception messages. Log internal details and return safe messages.
Step 11: Run the Application
mvn spring-boot:run
Health check:
curl http://localhost:8080/api/support/health
Expected output:
{
"service": "function-calling-support-assistant",
"status": "UP"
}
Check demo orders:
curl http://localhost:8080/api/support/orders
Expected output:
[
{
"orderId": "ORD-1001",
"customerEmail": "[email protected]",
"itemName": "Spring AI Course",
"status": "SHIPPED",
"amount": 49.99,
"estimatedDeliveryDate": "2026-06-25",
"cancellable": false
},
{
"orderId": "ORD-1002",
"customerEmail": "[email protected]",
"itemName": "Java Interview Guide",
"status": "PROCESSING",
"amount": 19.99,
"estimatedDeliveryDate": "2026-06-28",
"cancellable": true
}
]
Dates will differ because the sample data uses LocalDate.now().
Step 12: Test Real Use Cases
Use Case 1: Ask Order Status
Input:
curl -X POST http://localhost:8080/api/support/chat \
-H "Content-Type: application/json" \
-d '{
"message": "Where is my order ORD-1001?"
}'
Expected behavior:
- Model sees that it needs live order data.
- Model requests
getOrderStatus("ORD-1001"). - Spring AI executes the Java method.
- Model uses the result to answer.
Expected output:
{
"answer": "Order ORD-1001 for Spring AI Course has shipped. The estimated delivery date is June 25, 2026. This order is not cancellable because it has already shipped.",
"createdAt": "2026-06-23T10:15:30Z"
}
Use Case 2: Cancel a Cancellable Order
Input:
curl -X POST http://localhost:8080/api/support/chat \
-H "Content-Type: application/json" \
-d '{
"message": "Please cancel order ORD-1002. I bought it by mistake."
}'
Expected output:
{
"answer": "Order ORD-1002 has been cancelled. Reason: you bought it by mistake.",
"createdAt": "2026-06-23T10:16:10Z"
}
Verify the order:
curl http://localhost:8080/api/support/orders
ORD-1002 should now show:
{
"orderId": "ORD-1002",
"status": "CANCELLED",
"cancellable": false
}
Use Case 3: Try to Cancel a Shipped Order
Input:
curl -X POST http://localhost:8080/api/support/chat \
-H "Content-Type: application/json" \
-d '{
"message": "Cancel order ORD-1001."
}'
Expected output:
{
"answer": "Order ORD-1001 cannot be cancelled because its current status is SHIPPED.",
"createdAt": "2026-06-23T10:17:20Z"
}
The important part: the model does not decide cancellation rules. Your Java code does.
Use Case 4: Create a Support Ticket
Input:
curl -X POST http://localhost:8080/api/support/chat \
-H "Content-Type: application/json" \
-d '{
"message": "My order ORD-1003 arrived damaged. My email is [email protected]. Please create a high priority support ticket."
}'
Expected output:
{
"answer": "I created a high priority support ticket for your damaged item issue on order ORD-1003. Your ticket ID is TICKET-8F3A91BC.",
"createdAt": "2026-06-23T10:18:00Z"
}
Verify:
curl http://localhost:8080/api/support/tickets
Expected output:
[
{
"ticketId": "TICKET-8F3A91BC",
"customerEmail": "[email protected]",
"orderId": "ORD-1003",
"issue": "Order arrived damaged",
"priority": "HIGH",
"createdAt": "2026-06-23T10:18:00Z"
}
]
Use Case 5: Missing Required Data
Input:
curl -X POST http://localhost:8080/api/support/chat \
-H "Content-Type: application/json" \
-d '{
"message": "Create a support ticket for my damaged item."
}'
Expected output:
{
"answer": "I can help create a support ticket. Please provide your order ID and customer email address.",
"createdAt": "2026-06-23T10:19:00Z"
}
Good assistant behavior means asking for missing required information before calling a tool.
What Happens Behind the Scenes
flowchart TD
A["User message"] --> B["ChatClient"]
B --> C["Model receives tool schemas"]
C --> D{"Does model need a tool?"}
D -->|No| E["Model answers directly"]
D -->|Yes| F["Model requests tool call"]
F --> G["Spring AI executes @Tool method"]
G --> H["Tool result returned to model"]
H --> I["Model writes final answer"]
I --> J["API response"]
@Tool and @ToolParam
Spring AI uses @Tool to expose a Java method as a tool.
@Tool(description = "Get current order status for a given order ID")
public OrderDto getOrderStatus(
@ToolParam(description = "Order ID such as ORD-1001") String orderId
) {
...
}
Use @ToolParam to describe method parameters.
This helps the model understand:
- What values are required.
- What format to use.
- What the parameter means.
- How to call the method correctly.
Tools Can Retrieve Information or Take Action
| Tool Type | Example | Risk Level |
|---|---|---|
| Read-only tool | getOrderStatus |
Low |
| Action tool | cancelOrder |
Medium or high |
| Workflow tool | createSupportTicket |
Medium |
| Payment tool | refundPayment |
High |
| External API tool | bookShipmentPickup |
Medium or high |
For action tools, always validate in Java.
Security Rules
Do not let the model become your authorization layer.
For production:
- Authenticate the user.
- Check user ownership of the order.
- Validate all tool parameters.
- Keep allow-lists for action types.
- Require confirmation for high-impact actions.
- Log every tool call.
- Rate-limit sensitive tools.
- Never expose internal stack traces to the model or user.
Production Confirmation Pattern
For sensitive actions such as cancellation, refunds, transfers, or deletion, use a two-step pattern.
sequenceDiagram
participant User
participant Assistant
participant App
participant Tool
User->>Assistant: Cancel order ORD-1002
Assistant->>App: Prepare cancellation summary
App-->>User: Please confirm cancellation
User->>Assistant: Yes, confirm
Assistant->>Tool: cancelOrder(ORD-1002)
Tool-->>Assistant: Cancelled
Assistant-->>User: Order cancelled
In this tutorial, we call cancellation directly to keep the example simple. In production, add confirmation.
Common Mistakes
| Mistake | Problem | Better Approach |
|---|---|---|
| Tool description is too vague | Model may not call it correctly | Write clear descriptions |
| Exposing too many tools | Model can choose wrong tool | Keep tool set focused |
| Letting model decide business rules | Unsafe actions | Validate in Java |
| No user authorization | Data leak risk | Check ownership before tool execution |
| No logging | Hard to audit actions | Log tool name, user, arguments, result |
| Missing required data | Bad tool calls | Tell assistant to ask follow-up questions |
| Tools mutate data without confirmation | Risky UX | Add confirmation flow |
When To Use Function Calling
Use tool calling when the assistant needs:
- Live data.
- Private data.
- Database lookups.
- External API calls.
- Calculations.
- Business actions.
- Workflow creation.
- Ticket creation.
- Order cancellation.
- Status checks.
Do not use tool calling for simple explanations. Normal ChatClient prompts are enough for that.
Complete Test Script
curl http://localhost:8080/api/support/health
curl http://localhost:8080/api/support/orders
curl -X POST http://localhost:8080/api/support/chat \
-H "Content-Type: application/json" \
-d '{"message":"Where is my order ORD-1001?"}'
curl -X POST http://localhost:8080/api/support/chat \
-H "Content-Type: application/json" \
-d '{"message":"Please cancel order ORD-1002. I bought it by mistake."}'
curl -X POST http://localhost:8080/api/support/chat \
-H "Content-Type: application/json" \
-d '{"message":"Cancel order ORD-1001."}'
curl -X POST http://localhost:8080/api/support/chat \
-H "Content-Type: application/json" \
-d '{"message":"My order ORD-1003 arrived damaged. My email is [email protected]. Please create a high priority support ticket."}'
curl http://localhost:8080/api/support/tickets
Summary
You implemented function calling with Spring AI using a real customer support use case.
You learned how to:
- Create Java tools with
@Tool. - Describe parameters with
@ToolParam. - Pass tools to
ChatClientusing.tools(...). - Retrieve live order data.
- Execute a business action.
- Create a support ticket.
- Return a natural language answer to the user.
The key design rule:
The AI model can request a tool call, but your Spring Boot application owns validation, authorization, execution, and auditing.
That is what makes function calling useful for real enterprise applications.