{"slug": "building-an-ai-shirt-agent-with-spring-ai-and-tool-calling", "title": "Building an AI Shirt Agent with Spring AI and Tool Calling", "summary": "A developer published shirts-agent, an open-source Spring Boot and Spring AI example that shows Java developers how to wire an LLM into a real application using tool calling. The service accepts an item and color at a REST endpoint, validates input in plain Java before any model call, and has OpenAI's gpt-4o-mini invoke a @Tool-annotated method that searches an in-memory shirt catalog and returns a structured Shirt object. The project, built on Java 21, Spring Boot 3.5.6 and Spring AI 1.1.8, is available on GitHub.", "body_md": "Most tutorials that introduce large language models to Java developers start with a chatbot that answers questions. That is a useful starting point, but it hides the part that makes LLM features valuable inside a real application: the model has to take an action that your code controls. A chatbot that only produces text is a demo. An agent that looks something up in your own system and returns a structured result is a feature.\n\nThis article walks through `shirts-agent`, a small Spring Boot and Spring AI example that makes the idea concrete. A user sends an item and a color to a REST endpoint. An AI agent calls a Java method that searches an in-memory catalog, and the service returns a structured shirt recommendation. The whole project fits in a few classes, which makes it a good way to see how the pieces fit together before you build something larger.\n\nThe project uses Java 21, Spring Boot 3.5.6, Spring AI 1.1.8, OpenAI's `gpt-4o-mini` model, and Maven. The complete source is available at [github.com/josesaid/shirts-agent](https://github.com/josesaid/shirts-agent).\n\nA language model is trained on public text up to a certain date. It does not know what is in your product catalog, your inventory, your customer records, or your internal APIs. If you ask it \"which shirt do you recommend in red?\" without any extra help, it can only guess, and a guess is not a recommendation a shop can sell.\n\nThere are several ways to connect a model to your data:\n\n`shirts-agent` uses tool calling. The data is structured (a catalog of shirts with a color and a name), the question has a clear shape (find a shirt of this color), and the answer must come from your system rather than from the model's imagination. Tool calling is a natural fit.\n\nThe request flow is short and easy to follow. According to the project documentation, `ShirtController` receives the HTTP request, `ShirtService` validates the input, and `ShirtAgent` asks the model to find a shirt. The model then calls `ShirtCatalogTool.findShirtByColor` and returns a structured `Shirt` object. Invalid input is rejected with HTTP 400 before the model is ever called.\n\nThat last point deserves emphasis. Every call to a hosted model costs money and adds latency. Validation belongs in ordinary Java code, in front of the model, so that obviously bad requests never reach it.\n\nThe layers look like this:\n\n`POST /api/shirts/recommend` and maps the JSON body to a request object.`item` and `color` are present and well formed. Rejects bad input with a 400 response.`Shirt`.`@Tool`. This method is the only place where the model touches your data.\nKeeping these responsibilities separate pays off quickly. You can test the validation logic without a model, test the tool against the catalog without a model, and swap the model provider without rewriting the business logic.\n\nIn Spring AI, a tool is an ordinary Spring bean with a method that the model is allowed to call. The `@Tool` annotation provides a description that the model reads to decide when the tool is useful. The parameter names and descriptions matter, because the model relies on them to fill in the arguments correctly.\n\nAn illustrative version of such a tool looks like this. It is a simplified sketch to show the shape of the code, not a verbatim copy of the repository:\n\n```\n@Component\npublic class ShirtCatalogTool {\n\n    private final ShirtCatalog catalog;\n\n    public ShirtCatalogTool(ShirtCatalog catalog) {\n        this.catalog = catalog;\n    }\n\n    @Tool(description = \"Find a shirt in the catalog by its color\")\n    public Shirt findShirtByColor(\n            @ToolParam(description = \"The color of the shirt, for example red or blue\") String color) {\n        return catalog.findByColor(color)\n                .orElseThrow(() -> new IllegalArgumentException(\"No shirt found for color: \" + color));\n    }\n}\n```\n\nNotice what the tool does not do. It does not decide which shirt is best, it does not format prose, and it does not call the model. It takes a color, looks it up in a deterministic source of truth, and returns a typed result. The model is responsible for the conversation and the final wording. Your code is responsible for the facts.\n\nThis separation is the most important design habit when working with tools. Keep the tool small, deterministic, and easy to test. Put the intelligence where it belongs: in the orchestration of the model, not inside the data access.\n\nA recommendation that comes back as a paragraph of text is hard to use in an application. A frontend needs fields. A test needs assertions. A downstream service needs a predictable shape.\n\nSpring AI can ask the model to return its answer as a Java object. The agent declares the target type, the framework converts the model's response into that type, and your service receives a `Shirt` rather than a string it has to parse. This is what the project means by \"returns a structured `Shirt`.\"\n\nStructured output reduces an entire category of bugs. You no longer depend on regular expressions over free-form text, and a change in the model's phrasing cannot silently break your parsing code. If the model returns something that does not fit the expected type, the failure shows up as an error you can handle rather than as wrong data that reaches a user.\n\nThe service layer is where you protect both your budget and your users. In `shirts-agent`, the service checks the request before the agent is invoked. A missing item or color results in an HTTP 400 response, and the model is never called.\n\nThis pattern is worth copying into any LLM-backed feature. Ask yourself which inputs can be rejected with plain Java. Length limits, required fields, allowed values, and basic format checks all belong in front of the model. Each request you reject locally is a request you do not pay for, and a failure you can explain to the caller in a clear message.\n\nValidation is also a security measure. Free-form text sent to a model can contain instructions that try to change its behavior. You do not eliminate that risk by validating input, but you reduce the surface area. Constrain what the model is asked to do, and constrain what the tool can return.\n\nThe project is intentionally easy to run. You need JDK 21 and Maven 3.9 or later. Clone the repository, export your OpenAI key, and start the application with Maven:\n\n```\ngit clone https://github.com/josesaid/shirts-agent.git\ncd shirts-agent\nexport OPENAI_API_KEY=sk-...\nmvn spring-boot:run\n```\n\nThen send a request to the endpoint on port 8080:\n\n```\ncurl -X POST http://localhost:8080/api/shirts/recommend \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"item\":\"playera\",\"color\":\"red\"}'\n```\n\nThe unit tests run with `mvn test`, and they do not call the model. That detail matters. Testing an LLM application should not depend on a live API call for every build. Test the controller, the service validation, and the tool logic in isolation, and reserve live model calls for a small number of integration checks.\n\nA good example is honest about its boundaries. `shirts-agent` has no database. The catalog is an in-memory list of four shirts, which is enough to demonstrate the pattern and nothing more. There is no messaging layer, no event stream, and no cloud deployment. There is no Dockerfile or compose file. The application runs on port 8080 and depends on the OpenAI API.\n\nEach of these omissions is a useful discussion point. In production you would replace the in-memory catalog with a repository backed by a real database, perhaps with caching for frequently requested colors. You would add authentication to the endpoint, rate limits to protect your model budget, timeouts around the model call, and observability so you can see latency, token usage, and tool invocations. You would also decide how to handle a color that is not in the catalog: return a friendly message, suggest the closest available color, or ask the user to try again.\n\nThe point of the example is not to be production-ready. The point is to show the smallest set of moving parts that still demonstrates a real agent calling a real tool.\n\nSeveral practical lessons come out of a project this small.\n\n**Start from the data, not from the model.** Identify the facts your feature needs and where they live. If the answer depends on your catalog, your orders, or your policies, the model should retrieve those facts through a tool rather than generate them.\n\n**Make tools boring.** A tool should be a deterministic method with clear inputs and outputs. The less cleverness you put inside a tool, the easier it is to test, reason about, and trust.\n\n**Validate in code before you call the model.** Use the programming language you already know to reject bad input, enforce limits, and protect your budget.\n\n**Prefer structured output.** Typed results are easier to test and integrate than free-form text. Let the framework handle the conversion, and fail loudly when the shape is wrong.\n\n**Test without the model where you can.** Most of your application logic should run in unit tests without network calls. Reserve live calls for integration tests that you run deliberately.\n\n**Keep the model replaceable.** Spring AI abstracts the chat provider behind common interfaces. Keep your domain logic and tools free of provider-specific details so that changing models is a configuration decision rather than a rewrite.\n\nSpring AI brings the patterns Spring developers already know to LLM applications. Dependency injection wires the chat client and the tools. Configuration properties select the model and its settings. Auto-configuration sets up the common parts so that you can focus on your domain. The result feels familiar to any team that already builds Spring Boot services, which is the main reason to consider it for enterprise Java work.\n\nThis matters for teams in regulated or long-lived systems. The question is rarely whether a model can answer a question. The question is whether the feature can be built, tested, deployed, monitored, and maintained by the same engineers who maintain the rest of the platform. Using familiar tools reduces the cost of answering yes.\n\nIf you want to take this project further, there are several natural next steps that keep the same architecture.\n\nFirst, add more tools. A real shop needs to check stock, look up sizes, and calculate prices. Each of these can be another annotated method with a clear description. The model decides which tool to call based on the request, so give each tool a name and description that make its purpose obvious.\n\nSecond, add a second data source. Imagine a product search over a vector index for descriptions and reviews, combined with the exact catalog lookup you already have. Retrieval handles the fuzzy part of the question, and tools handle the exact facts. This hybrid approach is one of the most useful patterns in production LLM systems.\n\nThird, add evaluation. Create a set of requests with expected outcomes and run them regularly. When you change a prompt, a model, or a tool description, the evaluation tells you whether the behavior got better or worse. Without evaluation, LLM features drift in ways that are hard to notice until a user complains.\n\nFourth, add observability. Record which tools were called, how long each step took, and how many tokens were used. This information is essential for controlling cost and diagnosing unexpected answers.\n\n`shirts-agent` is a small project with a big lesson. A language model becomes useful in an application when it can call your code, and your code remains the source of truth. A controller accepts the request, a service validates it, an agent coordinates the model, and a tool returns the facts. Each layer has a clear job, and each can be tested on its own.\n\nIf you are a Java developer curious about AI features, this is a good place to start. Clone the repository, run the example, change the catalog, add a second tool, and watch how the model chooses between them. Then ask the question that matters for your own domain: which facts in my system should the model be allowed to ask for, and which rules should my code enforce before the model is ever involved?\n\nThe full source code and documentation are available at [github.com/josesaid/shirts-agent](https://github.com/josesaid/shirts-agent).", "url": "https://wpnews.pro/news/building-an-ai-shirt-agent-with-spring-ai-and-tool-calling", "canonical_source": "https://dev.to/said_olano/building-an-ai-shirt-agent-with-spring-ai-and-tool-calling-2kl4", "published_at": "2026-10-21 22:05:24+00:00", "updated_at": "2026-10-21 22:10:57.729034+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-agents", "ai-tools", "developer-tools"], "entities": ["Spring AI", "Spring Boot", "OpenAI", "gpt-4o-mini", "Java", "Maven", "shirts-agent", "GitHub"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/building-an-ai-shirt-agent-with-spring-ai-and-tool-calling", "markdown": "https://wpnews.pro/news/building-an-ai-shirt-agent-with-spring-ai-and-tool-calling.md", "text": "https://wpnews.pro/news/building-an-ai-shirt-agent-with-spring-ai-and-tool-calling.txt", "jsonld": "https://wpnews.pro/news/building-an-ai-shirt-agent-with-spring-ai-and-tool-calling.jsonld"}}