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:
- Calling OpenAI API directly - worked, but I had no structure. Just HTTP calls scattered everywhere.
- Using LangChain4j - better, but still felt like I was fighting the framework.
- 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
JavaClaw/├── base/ # Core: agent, tasks, tools, channels, config├── app/ # Spring Boot entry point, onboarding UI, web routes└── plugins/ └── telegram/ # Telegram long-poll channel pluginThe 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/├── 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-bucketedWhy 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.
spring: ai: openai: api-key: ${OPENAI_API_KEY} chat: options: model: gpt-4o temperature: 0.7Tomorrow, I switch to Ollama:
spring: ai: ollama: base-url: http://localhost:11434 chat: options: model: llama3.2No 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.
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.
# Weather Skill
When the user asks about weather:1. Extract the location from their message2. Call the weather API3. Return a formatted response
Tools: weather_api, geocodingThe 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.
# Start the application./gradlew :app:bootRun
# Access the onboarding flowopen http://localhost:8080/onboarding
# Access the chat interfaceopen http://localhost:8080/chat
# Monitor background jobsopen http://localhost:8081The 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:
| Component | Technology | Why |
|---|---|---|
| Language | Java 25 | Modern features, stable ecosystem |
| Framework | Spring Boot 4.0.3 | Battle-tested, huge community |
| LLM | Spring AI 2.0.0 | Provider abstraction, clean API |
| Modules | Spring Modulith 2.0.3 | Clean architecture enforcement |
| Jobs | JobRunr 8.5.0 | Background processing, dashboard |
| Database | H2 (embedded) | Simple, file-based |
| Templates | Pebble 4.1.1 | Lightweight, Java-friendly |
| Frontend | htmx 2.0.8 + Bulma 1.0.4 | No build step, responsive |
Getting Started
If you want to try this yourself:
- Clone the JavaClaw repository
- Set your OpenAI API key (or configure Ollama)
- Run
./gradlew :app:bootRun - Open
http://localhost:8080/onboarding - 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:
- Channels, not integrations - Don’t hardcode platforms. Make them implementations of an interface.
- Skills as files - Markdown is readable, editable, version-controllable.
- Provider flexibility - Spring AI’s abstraction lets you switch LLMs without code changes.
- 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