Full Stack • Java • System Design • Cloud • AI Engineering

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 @Tool methods 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:

  1. Model sees that it needs live order data.
  2. Model requests getOrderStatus("ORD-1001").
  3. Spring AI executes the Java method.
  4. 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:

  1. Authenticate the user.
  2. Check user ownership of the order.
  3. Validate all tool parameters.
  4. Keep allow-lists for action types.
  5. Require confirmation for high-impact actions.
  6. Log every tool call.
  7. Rate-limit sensitive tools.
  8. 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:

  1. Create Java tools with @Tool.
  2. Describe parameters with @ToolParam.
  3. Pass tools to ChatClient using .tools(...).
  4. Retrieve live order data.
  5. Execute a business action.
  6. Create a support ticket.
  7. 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.

References