Tools: Building a Minimal Telegram Bot Library for Java - Handler Chain Pattern

Tools: Building a Minimal Telegram Bot Library for Java - Handler Chain Pattern

Source: Dev.to

The Problem ## The Solution: Handler Chain ## Example Handler ## Why This Works ## Design Philosophy: Obvious Defaults ## Production Details ## What I Learned ## First Open Source Library ## When NOT to Use This ## Takeaway 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. Most Telegram bot libraries for Java come with: For a simple bot that monitors groups and sends notifications, this felt like overkill. I wanted: 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 core idea is simple - each handler decides if it processes an 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. No base classes. No decorators. Just: check condition → do work → return boolean. 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: Spring collects every @Component implementing UpdateHandler and passes them in. No manual registration needed. 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: 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. 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. 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. 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. 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. 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. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: @FunctionalInterface public interface UpdateHandler { boolean handle(Update update); } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @FunctionalInterface public interface UpdateHandler { boolean handle(Update update); } CODE_BLOCK: @FunctionalInterface public interface UpdateHandler { boolean handle(Update update); } CODE_BLOCK: @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 } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @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 } } CODE_BLOCK: @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 } } COMMAND_BLOCK: @Bean public UpdateDispatcher dispatcher(List<UpdateHandler> handlers) { return new UpdateDispatcher(handlers); // Spring injects all beans } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: @Bean public UpdateDispatcher dispatcher(List<UpdateHandler> handlers) { return new UpdateDispatcher(handlers); // Spring injects all beans } COMMAND_BLOCK: @Bean public UpdateDispatcher dispatcher(List<UpdateHandler> handlers) { return new UpdateDispatcher(handlers); // Spring injects all beans } CODE_BLOCK: TelegramBotPollingService service = new TelegramBotPollingService( apiClient, dispatcher, true // autoStart ); // Polling runs in background, shutdown hook stops it on exit Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: TelegramBotPollingService service = new TelegramBotPollingService( apiClient, dispatcher, true // autoStart ); // Polling runs in background, shutdown hook stops it on exit CODE_BLOCK: TelegramBotPollingService service = new TelegramBotPollingService( apiClient, dispatcher, true // autoStart ); // Polling runs in background, shutdown hook stops it on exit - Heavy abstractions (command frameworks, conversation flows, state machines) - Opinionated architectures - Dependencies you might not want - 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 - Need a proxy? Pass a custom OkHttpClient - Want different timeouts? Use TelegramApiConfig.builder() - Otherwise? Just use the constructor, it works - 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