Skip to content

How to Build a Personal AI Assistant with Java and Spring Boot

I wanted to build my own AI assistant. Not another Python chatbot, but something in Java that I could run locally and extend easily. Here’s what I learned.

The Problem

Most AI assistant tutorials are Python-based. LangChain this, LangGraph that. But my day job is Java. I know Spring Boot inside and out. Why should I switch languages just to build an AI assistant?

I tried a few things:

  1. Calling OpenAI API directly - worked, but I had no structure. Just HTTP calls scattered everywhere.
  2. Using LangChain4j - better, but still felt like I was fighting the framework.
  3. Building from scratch - too much boilerplate. I wanted to focus on features, not infrastructure.

Then I found Spring AI. And everything clicked.

What I Wanted

Before diving into code, I wrote down what I actually needed:

- Run on my machine (privacy matters)
- Talk to it via Telegram (I live in Telegram)
- Switch LLM providers easily (OpenAI today, Ollama tomorrow)
- Add new skills without touching core code
- See what it's doing (no black boxes)

The last point was crucial. I wanted to debug my assistant like any other application. Not through logs, but through human-readable files.

The Architecture

I settled on a modular design using Spring Modulith. Here’s why this matters.

Module Separation

Project structure
JavaClaw/
├── base/ # Core: agent, tasks, tools, channels, config
├── app/ # Spring Boot entry point, onboarding UI, web routes
└── plugins/
└── telegram/ # Telegram long-poll channel plugin

The base module contains everything that doesn’t depend on Spring Boot. Agent logic, task management, tool definitions. This separation means I can test core functionality without starting the whole container.

The app module is the actual Spring Boot application. It wires everything together.

The plugins folder is where magic happens. Want Slack support? Drop in a Slack plugin. Want Discord? Same thing.

The Workspace Concept

Instead of storing everything in a database, I use a file-based workspace:

Workspace structure
workspace/
├── AGENT.md # System prompt - customize personality
├── INFO.md # Environment context injected into every prompt
├── context/ # Agent memory and long-term context files
├── skills/ # Drop SKILL.md files here for runtime extension
└── tasks/ # Task files, date-bucketed

Why files? Because I can read them. I can edit them. I can version control them. When something goes wrong, I open a text file, not a database GUI.

Spring AI Integration

Here’s where Spring AI shines. I can switch providers with configuration, not code.

application.yml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o
temperature: 0.7

Tomorrow, I switch to Ollama:

application-ollama.yml
spring:
ai:
ollama:
base-url: http://localhost:11434
chat:
options:
model: llama3.2

No code changes. Just configuration.

The Channel Pattern

This was my breakthrough moment. Instead of hardcoding Telegram into the core, I made it a channel.

+-------------+ +-------------+ +-------------+
| Telegram |---->| Channel |---->| Agent |
| (plugin) | | Interface | | (core) |
+-------------+ +-------------+ +-------------+
^
|
+-------------+
| WebSocket |
| Chat UI |
+-------------+

The agent doesn’t know or care where messages come from. It just receives a Message and returns a Response. The channel handles the platform-specific details.

Channel Interface
public interface Channel {
void send(String conversationId, String message);
void onMessage(ConversationMessage message);
}

Telegram is just one implementation. WebSocket for the web UI is another. Want email? Implement the interface.

Extensible Skills

I didn’t want to recompile every time I added a capability. So I made skills configurable through Markdown files.

skills/weather.SKILL.md
# Weather Skill
When the user asks about weather:
1. Extract the location from their message
2. Call the weather API
3. Return a formatted response
Tools: weather_api, geocoding

The agent reads these files at startup. It knows what skills are available and how to use them. Adding a new skill is dropping a file. Removing it is deleting a file.

Background Jobs

AI tasks aren’t instant. Sometimes I want the assistant to work on something in the background while I do other things.

I used JobRunr for this. It integrates with Spring and provides a dashboard.

Starting the application
# Start the application
./gradlew :app:bootRun
# Access the onboarding flow
open http://localhost:8080/onboarding
# Access the chat interface
open http://localhost:8080/chat
# Monitor background jobs
open http://localhost:8081

The background dashboard shows me what’s running, what failed, and what’s queued. Essential for debugging.

What I Got Wrong

Let me save you some time with my mistakes.

Mistake 1: Hardcoding the LLM Provider

At first, I hardcoded OpenAI everywhere. Then I wanted to test with Ollama locally. Had to change 15 files. Don’t do this. Use Spring AI’s abstraction from day one.

Mistake 2: Storing Tasks in a Database

My first version used H2 for tasks. Worked fine until I wanted to see what my assistant was planning. SQL queries to debug an AI assistant? No thanks. Markdown files are better.

Mistake 3: Ignoring Modular Architecture

I started with a monolith. Everything in one package. Then I wanted to add Telegram support. And Slack. And a web UI. The single module became a mess. Spring Modulith forced me to think about boundaries.

Mistake 4: No Background Job Strategy

Early versions blocked the main thread while the AI “thought.” The UI froze. Telegram webhooks timed out. JobRunr fixed this, but I wish I’d planned for it from the start.

The Stack

For those who want the full picture:

ComponentTechnologyWhy
LanguageJava 25Modern features, stable ecosystem
FrameworkSpring Boot 4.0.3Battle-tested, huge community
LLMSpring AI 2.0.0Provider abstraction, clean API
ModulesSpring Modulith 2.0.3Clean architecture enforcement
JobsJobRunr 8.5.0Background processing, dashboard
DatabaseH2 (embedded)Simple, file-based
TemplatesPebble 4.1.1Lightweight, Java-friendly
Frontendhtmx 2.0.8 + Bulma 1.0.4No build step, responsive

Getting Started

If you want to try this yourself:

  1. Clone the JavaClaw repository
  2. Set your OpenAI API key (or configure Ollama)
  3. Run ./gradlew :app:bootRun
  4. Open http://localhost:8080/onboarding
  5. Follow the setup wizard

The onboarding flow creates your workspace, configures your agent, and sets up the default skills.

Privacy by Design

This was a requirement from day one. Everything runs on my hardware. My data doesn’t leave my network unless I configure an external LLM.

┌─────────────────────────────────────────────────────┐
│ Your Machine │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Agent │ │ Workspace│ │ Local LLM │ │
│ │ (core) │ │ (files) │ │ (Ollama) │ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
│ │
│ Data stays here unless you configure external API │
└─────────────────────────────────────────────────────┘

Even with an external LLM, I control what’s sent. The context files, the system prompt, the skills - all local.

Summary

Building an AI assistant in Java is not only possible, it’s practical. Spring AI handles the LLM abstraction. Spring Modulith keeps the architecture clean. JobRunr manages background tasks. Files instead of databases keep things transparent.

The key insights:

  1. Channels, not integrations - Don’t hardcode platforms. Make them implementations of an interface.
  2. Skills as files - Markdown is readable, editable, version-controllable.
  3. Provider flexibility - Spring AI’s abstraction lets you switch LLMs without code changes.
  4. Local-first design - Keep data on your hardware. Only externalize what you choose.

The result is an assistant I actually understand. I can see its memory, edit its skills, and debug its behavior. That’s worth more than any pre-packaged AI platform.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments