SKG Java Community

Explore · Learn · Engage · Socialize

A community of developers in Thessaloniki, sharing knowledge and exchanging ideas around Java and beyond - Kubernetes, LLMs, AI, GraphQL, Kafka, Docker, OpenTelemetry, CI/CD, and more.

Everyone is welcome to attend, present, socialize, exchange ideas, and make new connections!

Join us on Meetup Meetup
Eleftherios Chrysochoidis

Eleftherios Chrysochoidis

Technical Lead at Chubb · SKG Java Community Organizer

With 10+ years of experience in software engineering, Eleftherios (or simply Lef) has gained deep expertise in Java and Spring Boot technologies. He is an active organizer of local tech events in Thessaloniki, dedicated to fostering a vibrant community of software engineers. Always eager to talk and present at community events, aiming to share and spread knowledge.

Introduction

AI is rapidly becoming part of everyday applications - but how can Java developers actually use it in real-world systems?

In this hands-on workshop, we'll build an intelligent "Meetup Community Assistant" using Spring Boot, exploring how modern AI capabilities can be integrated directly into your backend services.

Starting from a simple Spring Boot application, we'll progressively evolve it into a fully capable AI agent that can understand user intent, interact with business logic, and automate workflows like event RSVPs.

What You'll Learn

About This Workshop

This workshop was presented at the SKG Java Meetup on Thursday, May 28, 2026 at Ok!Thess, Thessaloniki.

Basic Resources (to start with)

For more detailed, topic-specific resources (structured output, tool calling, chat memory, MCP, etc.), see the Wrap-up & Resources section at the end of this guide.

💻 Source Code

The complete source code for this workshop is available on GitHub: LefterisXris/spring-ai-meetup-demo. Clone it, follow along, or use it as a reference for your own projects.

Prerequisites


Step 0 Project Scaffold

In this step we generate the base Spring Boot project and verify it runs. No AI code yet - just the foundation.

Generate the Project

Go to start.spring.io and configure the project with the following settings:

SettingValue
ProjectMaven
LanguageJava
Spring Boot3.5.14 (latest stable)
Groupgr.skg.java.meetup
Artifactspring-ai
PackagingJar
ConfigurationYAML
Java25

Dependencies (use CTRL+B to add): Spring Web, Spring Data JPA, H2 Database, Lombok

Spring Initializr configuration
Spring Initializr with all settings configured

Click GENERATE (CTRL+Enter) to download the zip file.

Open the Project

Extract the downloaded zip and open it in your IDE.

💡 Tip

If you use PowerShell and IntelliJ often, you can set up a handy alias that extracts and opens in one command. Add this function to your PowerShell profile ($PROFILE):

# idea - Extract a Spring Initializr zip and open it in IntelliJ IDEA
# Usage: idea <zip-file> [destination]
function idea {
    param(
        [string]$ZipPath,
        [string]$Dest = "C:\Development\Code"
    )
    $name = [System.IO.Path]::GetFileNameWithoutExtension($ZipPath)
    $target = Join-Path $Dest $name
    Expand-Archive -Path $ZipPath -DestinationPath $Dest
    & "idea64.exe" $target
    Remove-Item $ZipPath
}
idea command in PowerShell
idea .\spring-ai.zip - one command to extract and open

Verify It Works

Run the main class SpringAiApplication. Check the console for:

Started SpringAiApplication in 4.362 seconds (process running for 5.32)
Application running in IntelliJ
Application started successfully in IntelliJ

Add a Simple Endpoint

Create JavaController.java to confirm Spring Web is wired up:

@RestController
public class JavaController {

   @GetMapping("/hi")
   public String hello() {
      return "Hello SKG Java Meetup!";
   }
}
Hello endpoint working
GET /hi returning "Hello SKG Java Meetup!" - HTTP 200

Initialize Git

git init
git branch -m master main
git add .gitignore .gitattributes .mvn/ mvnw mvnw.cmd pom.xml src/
git commit -m "chore: initial Spring Boot scaffold from Spring Initializr"
📝 Note

The .gitignore generated by Spring Initializr already excludes IDE files (.idea/, .vscode/), build output (target/), and other artifacts.


Step 1 Simple Chat with ChatClient

In this step we add Spring AI to the project and create our first AI-powered endpoint. With just a few lines of code, we'll be talking to an LLM from a Spring Boot controller.

Documentation

Add Spring AI Dependencies

Add the Spring AI BOM and the OpenAI starter to pom.xml:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-bom</artifactId>
      <version>1.0.8</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

Configure the API Key

⚠️ Important

API keys should always be injected via environment variables - never hardcoded in source files. If the API key is not set, the application will fail to start with: OpenAI API key must be set

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY} # always inject from env vars
      chat:
        options:
          model: gpt-4o-mini
Setting API key in IntelliJ
Setting OPENAI_API_KEY in IntelliJ Run Configuration

Add the /chat Endpoint

@RestController
public class JavaController {

   private final ChatClient chatClient;

   public JavaController(ChatClient.Builder builder) {
      this.chatClient = builder.build();
   }

   @GetMapping("/chat")
   public String chat(@RequestParam String message) {
      return chatClient.prompt()
            .user(message)
            .call()
            .content();
   }
}

5 lines to talk to an LLM.

📝 Note on AiConfig

When you only have a single AI provider in your dependencies, Spring AI auto-configures a default ChatClient.Builder bean. No AiConfig class needed. However, with multiple providers (OpenAI + Anthropic), you'll need to explicitly specify which ChatModel to use.

Chat response
ChatClient responding to "Hi from SKG Java Meetup!"

Add a System Prompt

A system prompt sets the AI's personality and behavior for all interactions:

public JavaController(ChatClient.Builder builder) {
   this.chatClient = builder
         .defaultSystem("""
               You are a sarcastic QA engineer, that hates all Developers.
               Speak only with riddles and be rude. Be direct and short.
               """)
         .build();
}
Sarcastic system prompt
"What's more pointless than a meeting of minds that can't even meet a deadline?"

Prompt Decoration

Enhance individual request prompts with additional context using parameterized templates:

@GetMapping("/chat/suggest")
public String suggestion(@RequestParam String message) {
   return chatClient.prompt()
         .user(u -> u.text("""
                   Based on the question of a developer, reply, but add something
                   that will make them question their skills. ...
                   Developer's Question: {message}
                   """).param("message", message))
         .call()
         .content();
}
Prompt decoration
Decorated prompt combining system persona with per-request instructions
💡 Key Takeaway

The system prompt sets the overall personality, while prompt decoration shapes individual requests. Together, they give you fine-grained control over AI behavior without changing any model settings.


Step 2 Structured Output

What if you want the response as a typed Java object instead of raw text? Spring AI makes this remarkably simple.

Define the DTOs

record ProgrammingLanguageDTO(String name, String creator, LocalDate createdAt, Double popularity) {}
record ProgrammingLanguageSuggestion(String reason, ProgrammingLanguageDTO language) {}

Swap .content() for .entity()

The key change is just one line:

.call()
.entity(ProgrammingLanguageSuggestion.class);

How It Works Behind the Scenes

  1. Schema generation: Inspects the Java record and generates a JSON Schema
  2. Prompt augmentation: Appends the schema as instructions to the prompt
  3. Deserialization: The LLM's JSON response is deserialized via Jackson
Structured JSON output
No parsing. No regex. A typed Java record, straight from the AI.
📝 More Info

See the official docs on Structured Output Converters for details.

Peeking Under the Hood with Debug Logging

Add SimpleLoggerAdvisor and enable DEBUG logging to see exactly what Spring AI sends to the LLM:

logging:
  level:
    org.springframework.ai.chat.client.ChatClient: DEBUG
    org.springframework.ai.chat.client.advisor: DEBUG
Debug log with schema
The debug log reveals the full prompt: system message, user message, output format instructions, and the JSON Schema

Step 3 Chat Memory

So far, every request is stateless. In this step, we add conversational memory so the AI can maintain context across requests.

Add the MessageChatMemoryAdvisor

public JavaController(ChatClient.Builder builder,
                      ChatMemory chatMemory) {
   this.chatClient = builder
         .defaultSystem("""...""")
         .defaultAdvisors(new SimpleLoggerAdvisor(),
               MessageChatMemoryAdvisor.builder(chatMemory).build())
         .build();
}

Use conversationId

.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
Chat memory first request
First request: AI suggests HTML as the "worst" frontend language
Chat memory follow-up
Follow-up: AI remembers it recommended HTML and defends its choice
⚠️ LLMs Do Not Remember Anything

LLMs are stateless. Chat memory is an illusion - Spring AI stores all previous messages locally and replays the entire conversation history in every new request. This comes with trade-offs:

  • Token cost: Every previous message is re-sent on every call
  • Context window limits: Long conversations will exceed the limit
  • Hallucination risk: Longer context leads to more contradictions

This is why MessageWindowChatMemory uses a sliding window (maxMessages). Use chat memory intentionally.

Persisting Chat Memory

By default, memory uses InMemoryChatMemoryRepository (a ConcurrentHashMap, lost on restart). To persist, add one of:

Source: Chat Memory :: Spring AI Reference


Step 4 Domain Model - The Meetup Community

Before we can let the AI interact with our business logic, we need some business logic. In this step we build the Java Meetup Community domain - entity, repository, service, REST controller, and sample data with 20 Greek community members.

The Member Entity

@Data @Entity
public class Member {
   @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;
   private String fullName;
   private String bio;
   private String email;
   private LocalDate joinedAt;
   private Integer numberOfRsvps;
}

Database Configuration

spring:
  datasource:
    url: jdbc:h2:mem:spring-ai-skj-java-meetup
  h2:
    console:
      enabled: true
  jpa:
    hibernate:
      ddl-auto: create-drop
    defer-datasource-initialization: true
  sql:
    init:
      mode: always
Domain model working
GET /meetup/members/top?limit=3 - top attendees returned as JSON

Step 5 Tool Calling / Function Calling

This is the centerpiece of the workshop. Tool calling lets the AI invoke your actual Java business logic autonomously based on natural language requests.

How Tool Calling Works

Tool calling flow
The 6-step tool calling flow: Request → AI Model → Dispatch → Tool → Result → Response
  1. Chat Request with Tool Definitions (name, description, input schema) is sent to Spring AI
  2. Spring AI forwards prompt + tool definitions to the AI Model
  3. AI decides to call a tool - Spring AI dispatches the call to the actual Tool
  4. The Tool executes and returns a result
  5. Spring AI sends the tool result back to the AI Model
  6. AI generates the final Chat Response using the tool's data
⚠️ Why Descriptions Matter

@Tool and @ToolParam descriptions are critically important - they are the only way the LLM knows what each tool does and what each parameter means. Think of descriptions as API documentation, but your consumer is an AI, not a human developer.

Create the MeetupTools

@Component
public class MeetupTools {

   private final MeetupService meetupService;

   @Tool(description = "Get all members of the SKG Java Meetup community")
   public List<Member> getAllMembers() {
      return meetupService.getAllMembers();
   }

   @Tool(description = "Search members by name...")
   public List<Member> searchMembersByName(
         @ToolParam(description = "The name or part of the name to search for")
         String name) {
      return meetupService.searchMembersByName(name);
   }

   @Tool(description = "RSVP a member to the next meetup event...")
   public String rsvpToEvent(
         @ToolParam(description = "The email address of the member to RSVP")
         String email) {
      return meetupService.rsvpToEvent(email);
   }
   // + getTopAttendees, getMembersSince, findMemberByEmail
}

Wire It Up

.defaultTools(meetupTools)  // registers all @Tool methods

Demo: A Full Conversation

Member count
"How many members?" - AI calls getAllMembers() and responds: 20 members
Community history
"How long has it existed?" - AI uses system context: 14+ years, 60+ events
Organizer lookup
"Who is the organizer?" - AI finds Eleftherios, returns bio, RSVPs, email
RSVP business logic
"Add 3 more RSVPs for him" - AI calls rsvpToEvent three times. Total: 61.

The AI Understands Intent

We never explicitly said "search for Manolis and then RSVP him" - just described what happened:

GET /meetup/ai/chat?message=Mr Manolis has registered to attend an event. Let's update our records accordingly.
Intent understanding
AI understood the intent, searched for Manolis, then RSVPed him

Enable tool execution logs with:

logging.level.org.springframework.ai.tool: DEBUG
Tool execution logs
Logs: searchMembersByName("Manolis") → rsvpToEvent("theodoros.manolis@example.com")

Step 6 MCP Server

MCP (Model Context Protocol) turns your Spring Boot app into a server that any MCP-compatible AI client can discover and use. Three small changes:

1. Dependency:

<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>

2. Configuration:

spring.ai.mcp.server:
  name: skg-java-meetup-mcp
  type: sync
  version: 1.0.0

3. Register tools:

@Bean
ToolCallbackProvider meetupMcpTools(MeetupTools meetupTools) {
   return MethodToolCallbackProvider.builder()
         .toolObjects(meetupTools).build();
}

Exploring with MCP Inspector

npx @modelcontextprotocol/inspector
MCP Inspector starting
MCP Inspector starts and provides a local URL
MCP Inspector tools
All 6 tools visible with descriptions and parameters - exactly what any MCP client sees

Connecting to Claude Code

Claude Code MCP setup
Claude Code creates .mcp.json automatically
MCP connected
/mcp shows skg-java-meetup-mcp connected with 6 tools
MCP details
MCP tools list
MCP tool detail
Tool detail: getTopAttendees with its description and parameters

Demo: Claude Code Using Your MCP Tools

Claude querying via MCP
"Who has attended the most?" - Claude calls getTopAttendees via MCP
Claude RSVPing via MCP
Claude RSVPs Konstantinos Makris (56 RSVPs) and returns his contact details

Wrap-up

Step 0 - Scaffold

Spring Initializr, project structure, Git init

Step 1 - Chat

ChatClient, system prompt, prompt decoration

Step 2 - Structured

.entity(), Java records as response types

Step 3 - Memory

MessageChatMemoryAdvisor, conversationId

Step 4 - Domain

JPA entities, services, REST endpoints

Step 5 - Tools

@Tool, @ToolParam, AI calling business logic

Step 6 - MCP

Exposing tools to external AI clients

Key Takeaways

💻 Source Code

The complete source code for this workshop is available on GitHub: LefterisXris/spring-ai-meetup-demo. Clone it, follow along, or use it as a reference for your own projects.

Resources


Get Involved

SKG Java Community

Explore · Learn · Engage · Socialize

Our community brings developers together through technical talks, hands-on sessions, networking, and open discussions in a friendly environment.

🤝 Sponsorship

A big thanks to all our sponsors who have supported our events! We are always looking to welcome new sponsors and collaborators. If your company would like to support our community, host a meetup, or collaborate on events, please reach out to us. Your involvement helps us keep the SKG Java Community active, inclusive, and growing!

Appearance