I needed a Telegram bot for a side project. Looked at existing Java libraries - they're packed with features I didn't need. All I wanted was to send API requests and route updates to handlers based on logic I control. So I built my own thin wrapper.
The Problem
Most Telegram bot libraries for Java come with:
- Heavy abstractions (command frameworks, conversation flows, state machines)
- Opinionated architectures
- Dependencies you might not want
For a simple bot that monitors groups and sends notifications, this felt like overkill. I wanted:
- Direct access to Telegram API
- Custom routing logic without fighting a framework
- Minimal dependencies (just HTTP client + JSON)
- Works standalone or drops into Spring Boot
But the real issue wasn't features - it was cognitive overhead. Every library has its own mental model: "our way of structuring commands," "our conversation state system," "our middleware pipeline." You spend time learning the library's abstractions, hunting through docs for configuration options, debugging implicit behaviors that aren't mentioned anywhere.
And then you hit the edge cases. The library does 90% of what you need, but that last 10% requires fighting the framework. You're three layers deep in someone else's architecture trying to figure out why your handler isn't firing, or why metrics are being logged to a format you don't use.
Sometimes it's easier to write your own "bicycle" - but one that takes you from point A to point B, not one that tries to perform heart surgery and deliver a newspaper on the way. I just wanted to call Telegram API and route updates. Why learn someone else's architecture for that?
The Solution: Handler Chain
The core idea is simple - each handler decides if it processes an update:
@FunctionalInterface
public interface UpdateHandler {
boolean handle(Update update);
}
Return true → "I handled it, stop the chain"
Return false → "Not mine, try next handler"
That's the entire pattern. No magic, no annotations, no forced structure.
Example Handler
@Component
public class StartCommandHandler implements UpdateHandler {
private final TelegramApiClient apiClient;
@Override
public boolean handle(Update update) {
if (update.getMessage() == null ||
!"/start".equals(update.getMessage().getText())) {
return false; // not my update
}
apiClient.sendMessage(chatId, "Hello!");
return true; // handled, stop chain
}
}
No base classes. No decorators. Just: check condition → do work → return boolean.
Why This Works
Single Responsibility: Each handler has one job. StartCommandHandler only cares about /start. ButtonsCallbackHandler only cares about button clicks. They don't know about each other.
Composable: Dispatcher is itself an UpdateHandler. You can nest dispatchers, filter updates through pre-handlers, build trees of logic - it's just function composition.
Spring-friendly: In Spring Boot, all handlers auto-wire:
@Bean
public UpdateDispatcher dispatcher(List<UpdateHandler> handlers) {
return new UpdateDispatcher(handlers); // Spring injects all beans
}
Spring collects every @Component implementing UpdateHandler and passes them in. No manual registration needed.
Design Philosophy: Obvious Defaults
I wanted zero cognitive load to get started. Call new TelegramApiClient(token) and it just works. Connection pooling? Configured. Retry logic? Enabled. Polling timeout edge cases? Handled automatically.
You only configure when defaults don't fit:
- Need a proxy? Pass a custom
OkHttpClient - Want different timeouts? Use
TelegramApiConfig.builder() - Otherwise? Just use the constructor, it works
Same with handlers - no decorators, no registration APIs, no config files. Implement the interface, return a boolean, done.
Adding features means adding classes, not complexity. Want button handling? Write ButtonsCallbackHandler. Want admin commands? Write AdminCommandHandler. Each new feature is a new handler class - the core stays unchanged.
This means the library has limited built-in functionality. But that's intentional. I'd rather give you 5 simple building blocks than one complicated configuration system that tries to handle everything.
Production Details
A minimal library still needs production-grade internals:
Connection pooling: OkHttp with configurable pool size, keep-alive, timeouts. Defaults tuned for Telegram API (single host, long-lived connections).
Retry logic: Exponential backoff for transient errors (network issues, 5xx responses). But NOT for getUpdates - long polling already has offset-based recovery built in.
Polling timeout quirk: This took me hours to debug. Telegram holds the long polling connection for up to 30 seconds if no updates arrive. If OkHttp's readTimeout ≤ pollingTimeout, it kills the connection before Telegram responds, and the polling loop stops.
The library handles this automatically - it dynamically adjusts readTimeout per-request to always exceed the polling timeout by 35 seconds. Works regardless of what timeout value you configure.
Graceful shutdown: stopPolling() interrupts the polling thread immediately via thread.interrupt(). No waiting for the 30-second timeout to expire.
TelegramBotPollingService service = new TelegramBotPollingService(
apiClient, dispatcher, true // autoStart
);
// Polling runs in background, shutdown hook stops it on exit
What I Learned
Simplicity scales. The library is ~500 lines of code. No command router, no conversation state, no wizard builders. Just: here's the API, here's the update, return true/false. Users build their own patterns on top.
OkHttp internals matter. That readTimeout > pollingTimeout bug cost me hours. The fix is simple once you know it, but debugging "why does my bot stop every 30 seconds" was painful.
Spring's auto-wiring is powerful. Letting Spring inject List<UpdateHandler> means users just write @Component handlers and they auto-register. Zero configuration boilerplate.
Open source infrastructure is real work. I thought "publish to GitHub" meant pushing code. Turns out you also need: proper Maven POM, package repository setup, README with examples, social preview images, release notes. The code was maybe 60% of the effort.
First Open Source Library
This is my first time publishing an open source project. I built it for myself, then spent time cleaning it up, writing documentation, and setting up proper packaging. Not gonna lie - there's some impostor syndrome around releasing code publicly, even though I use it in production and it works fine.
But that's kind of the point of open source, right? Share what works for you, maybe it helps someone else. If it doesn't, no harm done.
Repository: https://github.com/nomad4tech/telegrambot4j
Example project: https://github.com/nomad4tech/telegrambot4j-demo
The demo shows handlers for commands, inline buttons, and callback handling. Copy-paste starting point if you want to try it.
When NOT to Use This
If you need:
- Complex conversation flows with state tracking → use a framework
- Built-in command parsing, permissions, middleware → heavier libraries have this
- Webhooks → currently only long polling supported
This library is for people who want direct API access with minimal abstraction. If you're building a complex chatbot with branching conversations, you probably want more structure than this provides.
Takeaway
Not every project needs a framework. Sometimes the best abstraction is almost none at all - just enough structure to avoid repeating yourself, not so much that it dictates how you work.
If you're building a Telegram bot in Java and existing libraries feel too heavy, give this a shot. And if you find bugs or want features, PRs welcome - learning as I go here.
Top comments (0)