A production-oriented learning project for building a multi-user REST agent with Spring Boot, Spring AI, Anthropic Claude, bounded conversation memory, and Java tool calling. It intentionally keeps the controller thin, makes ownership explicit, and isolates provider details in one service.
graph TD
Client --> Controller
Controller --> AgentService
AgentService --> ConversationService
AgentService --> ChatClient
ChatClient --> MemoryAdvisor
MemoryAdvisor --> ChatMemory
ChatClient --> Claude[Anthropic Claude]
Claude -->|tool call when useful| JavaTools
JavaTools --> Claude
Claude --> Controller
ChatModel is Spring AI's provider-neutral model abstraction. The Anthropic starter creates its Claude implementation from spring.ai.anthropic.* properties. ChatClient is a fluent facade over that model: the injected builder follows the builder pattern, allowing shared system instructions and advisors to be configured once. A prompt combines system and user messages; call().content() returns the final text.
The model is selected by spring.ai.anthropic.chat.options.model. temperature controls randomness (0.2 favors consistent answers/tool decisions) and max-tokens limits generated output, latency, and cost. Set ANTHROPIC_API_KEY in the environment—never commit it.
com.rajeshkawali.aiagent
├── config # Spring bean wiring
├── controller # HTTP adapters only
├── dto # immutable API contracts
├── exception # safe error translation
├── memory # Spring AI memory adapter
├── model # domain metadata
├── prompts # reusable system instructions
├── service # orchestration and ownership rules
└── tools # deterministic capabilities available to Claude
sequenceDiagram
participant U as User
participant C as Controller
participant S as AgentService
participant M as ChatMemory
participant A as Claude
U->>C: POST /chat (userId, conversationId, message)
C->>S: validated ChatRequest
S->>S: verify conversation ownership
S->>M: load bounded history by conversationId
S->>A: system prompt + history + message
A-->>S: final response
S->>M: advisor stores completed turn
S-->>U: ChatResponse
MessageWindowChatMemory stores framework message objects keyed by conversation ID. It fetches a sliding window before each model call and updates it after the call, preserving context while bounding token growth. The supplied InMemoryChatMemoryRepository loses data on restart and is local to one process. For real deployments, retain ConversationMemoryService and replace the repository with a supported JDBC, Redis, MongoDB, Cassandra, or Neo4j implementation. The separate ConversationService records who owns each ID; every read, delete, and chat verifies ownership.
Spring AI requires an explicit ChatMemory.CONVERSATION_ID for memory advisors. This project passes it on every chat call—there is no accidental shared default conversation.
flowchart LR
Q[User request] --> L[Claude sees tool schemas]
L --> D{Tool useful?}
D -- Yes --> J[Spring AI invokes @Tool Java method]
J --> R[Tool result]
R --> L
D -- No --> F[Final response]
L --> F
Each public capability has @Tool(description = ...). When AgentService supplies tool objects through .tools(...), Spring AI derives schemas from those methods and sends them to Claude. Claude chooses whether to issue a tool call; Spring AI executes the matching Java method, returns the result to Claude, and Claude writes the user-facing response. This is why calculator and text answers are exact rather than model guesses.
Available tools:
CalculatorTool: add, subtract, multiply, divide (zero is rejected).DateTimeTool: UTC date, time, and date-time.WeatherTool: clearly labelled mock weather.CurrencyTool: clearly labelled fixed-rate mock conversion, never financial advice.TextTool: word/character count and uppercase/lowercase transforms.
Production tools should validate inputs, have timeouts/retries around external calls, log only safe metadata, and return errors suitable for the model to explain. Tool objects are explicitly registered per request rather than globally exposing arbitrary beans.
AgentSystemPrompt is a component so the stable system instruction is central, reviewable, and reusable. It tells the agent to use supplied tools for deterministic facts, never invent results, and label mock outputs. Keep system prompts short, specific, and tested; do not place secrets or untrusted user input in them. Application-level authorization and validation remain the real guardrails—the prompt is not a security boundary.
Requirements: Java 21 and Maven 3.9+.
set ANTHROPIC_API_KEY=your_key_here
mvn spring-boot:runOn PowerShell:
$env:ANTHROPIC_API_KEY = 'your_key_here'
mvn spring-boot:runapplication.yml documents every non-default property inline. Override the model, token limit, or memory window with your normal Spring profile/environment configuration.
Create a conversation first:
curl -X POST http://localhost:8080/conversation/new -H "Content-Type: application/json" -d '{"userId":"alice"}'Response:
{"id":"e8...","userId":"alice","createdAt":"2026-08-03T10:00:00Z"}Send a chat turn (Claude can decide to use a tool):
curl -X POST http://localhost:8080/chat -H "Content-Type: application/json" -d '{"userId":"alice","conversationId":"e8...","message":"What is 19.5 multiplied by 4?"}'Read or delete history; userId is deliberately required for ownership validation:
curl "http://localhost:8080/conversation/e8...?userId=alice"
curl -X DELETE "http://localhost:8080/conversation/e8...?userId=alice"
curl http://localhost:8080/healthEndpoints: POST /conversation/new allocates an owned ID; POST /chat executes an agent turn; GET /conversation/{id} returns bounded history; DELETE /conversation/{id} deletes metadata and memory; GET /health is a dependency-free liveness check.
GlobalExceptionHandler provides consistent JSON errors for validation (400), absent conversations (404), cross-user access (403), and unexpected failures (500). The application logs request IDs/lengths and response lengths rather than message contents, which avoids casually recording potentially sensitive prompts. Add correlation IDs and an audit policy in production.
Tool unit tests verify deterministic behavior. HealthControllerTest is a MockMvc web slice test: it validates the HTTP contract without requiring an Anthropic API key or live model. Service-level testing should mock ChatClient or run against a provider test double; real-provider integration tests should be separately profiled and disabled by default to protect credentials and spend.
Run all tests:
mvn test- Startup cannot authenticate: ensure
ANTHROPIC_API_KEYexists in the process environment and restart the app. - 404 on chat: create the conversation first and use the exact returned ID.
- 403: the
userIddoes not own the conversation. - Memory disappears: expected with the in-memory learning implementation; use a persistent repository.
- No tool invocation: tool use is model-directed. Phrase tasks that need exact computation/current data and retain the tool descriptions.
Recommended evolutions: Spring Security/OAuth identity instead of request userId; a persistent conversation repository and Spring AI memory repository; actuator/metrics/tracing; rate limiting; streaming endpoint; idempotency keys; provider fallback; semantic long-term memory; integration tests in a protected CI profile.

