Easygram Architecture
System Overview
Easygram is built on a modular, multi-module Maven architecture where each module has a specific responsibility. This design ensures flexibility, testability, and easy customization.
Your Bot (@BotController with handler methods)
Update (from any transport)
Telegram Update → Filter Chain (ordered by priority)
BotContextSetterFilter (resolve User + Chat)
BotUpdatePublishingFilter (forward to broker, optional)
BotApiMethodsSenderFilter (execute response API calls)
BotDispatcher (route to matching handler)
Tier 1: State Handlers (@BotChatState with values)
Tier 2: Spec Handlers (no state restriction)
Tier 3: Default Handlers (@BotDefaultHandler)
Handler Method Invocation
Argument Resolution (inject User, Chat, params, etc)
Method Execution
Return-Type Handling (convert to BotApiMethod calls)
Response to Telegram API
Module Structure
core-api
Pure contracts—no Spring configuration, no business logic.
Contains:
- All custom annotations (
@BotCommand,@BotText, etc.) - Handler interfaces (
BotFilter,BotArgumentResolver,BotReturnTypeHandler) - Model classes (
BotRequest,BotResponse,Update,User,Chat) - Configuration interfaces (
BotConfigurer)
Every module depends on core-api.
core
The processing engine.
Contains:
Botabstract base classBotDispatcher(routes updates to handlers)- Filter chain implementation
- Built-in
BotArgumentResolverimplementations - Built-in
BotReturnTypeHandlerimplementations - Handler scanning and registration (
BotHandlerLoader,BotHandlerRegistry) - Spring auto-configuration (
CoreAutoConfiguration)
Depends on: core-api, core-chatstate
core-chatstate
Optional chat state service (in-memory default).
Contains:
InMemoryBotChatStateService(thread-safe, for development/single-instance)
Replaceable with custom BotChatStateService implementations (Redis, JDBC, etc.) by providing your own @Bean.
Depends on: core-api
core-i18n
Internationalization support.
Contains:
BotMessageSource(locale-aware message lookup)BotKeyboardFactory(localized keyboard creation)LocalizedReplyreturn type- Locale argument resolver
Depends on: core-api
core-observability
Observability hooks.
Contains:
- Metrics collection interfaces
- Tracing integration points
Depends on: core-api
Transports
- longpolling — Polling updates from Telegram API (default)
- webhook — Receiving updates via HTTPS webhooks (Spring MVC)
- Kafka consumer transport — included in
messaging-api - RabbitMQ consumer transport — included in
messaging-api
Brokers / Messaging
- messaging-api — Unified broker module:
BotUpdatePublisherSPI, Kafka publisher (KafkaTemplate), RabbitMQ publisher (RabbitTemplate), smart routing, and both consumer transports. All broker functionality consolidated since 0.0.5.
spring-boot-starter
One-stop dependency.
Pulls in:
core,core-chatstate,core-i18n,core-observability- All transports (
longpolling,webhook) messaging-api(Kafka + RabbitMQ publishing and consuming)
samples
Runnable Spring Boot applications demonstrating each transport and pattern.
Update Processing Pipeline
1. Transport-Specific Bot (e.g., LongPollingBot)
Receives updates from Telegram and calls Bot.handleUpdate(Update).
2. Filter Chain
Each update passes through an ordered chain of BotFilter implementations:
public interface BotFilter {
void doFilter(BotRequest request, BotResponse response, BotFilterChain chain) throws Exception;
int getOrder(); // Lower value = higher priority
}
Built-in filters (in order):
- BotMdcFilter (order
Integer.MIN_VALUE) — Set MDC keys:bot.update.id,bot.transport,bot.user.id,bot.chat.id - BotContextSetterFilter (order
MIN_VALUE+1) — ResolveUserandChatfrom update - BotObservabilityFilter (order
MIN_VALUE+2) — Wrap update in MicrometerObservation - BotApiMethodsSenderFilter (order
MIN_VALUE+3) — Execute queued API calls - BotUpdatePublishingFilter — Publish update to broker (if enabled)
Custom filters can be inserted at any priority level.
3. Handler Dispatch (Three Tiers)
The dispatcher searches in order, stopping at first match:
Tier 1: State Handlers
- Methods with
@BotChatStatewhere at least one state value matches user's current state - Always executed first (highest priority)
- Ensures stateful handlers take precedence
Tier 2: Bot Handlers
- Spec-type handlers (
@BotCommand,@BotText,@BotCallbackQuery, etc.) - No state restriction
- Executed only if no Tier 1 match
Tier 3: Default Handlers
@BotDefaultHandler,@BotTextDefault,@BotDefaultCallbackQuery, etc.- No state restriction
- Executed only if no Tier 1 or Tier 2 match
- Fallback for unmatched updates
4. Handler Method Invocation
Argument Resolution
The framework resolves method parameters automatically:
@BotCommand("/start")
public String handleStart(
User user, // Resolved from Update
@BotCommandValue String command, // The matched "/start"
@BotCommandQueryParam Integer code // Query param (e.g., /start 42)
) { ... }
All resolvable parameters:
Update— Raw Telegram updateUser— SenderChat— Chat contextBotRequest— Internal request (filter context); also carriessetAttribute/getAttributefor cross-cutting dataBotResponse— Internal response accumulator; also carriessetAttribute/getAttributeTelegramClient— API clientBotMetadata— Bot's own info (id, username)Throwable— Exception being handled (in@BotExceptionHandleronly)Contact— Shared contact (in@BotContacthandlers only)Location— Shared location (in@BotLocationhandlers only)BotMarkupContext— Markup factory params (in@BotMarkupfactory methods only)Locale— User's locale (requirescore-i18n)- Annotated scalars (
@BotCommandValue,@BotTextValue,@BotCallbackQueryData,@BotCommandQueryParam) - Custom types via
BotArgumentResolver
Method Execution
Handler method is invoked with resolved arguments. Any exceptions are caught and routed to @BotExceptionHandler methods if defined.
Return-Type Handling
The return value is converted to BotApiMethod calls:
// Return type → Handler behavior
String → SendMessage to current chat
PlainReply → SendMessage + optional keyboard
PlainTextTemplate → SendMessage with #{index} token substitution
BotApiMethod<?> → Enqueued and executed directly
Collection<BotApiMethod<?>> → All executed in insertion order
Collection<Object> → Per-element dispatch to matching handler
void / null → No response sent
LocalizedReply → MessageSource key lookup + SendMessage (core-i18n)
LocalizedTemplate → Mixed ${key}/#{index} template + SendMessage (core-i18n)
5. Response Execution
Queued BotApiMethod calls are executed by BotApiMethodsSenderFilter.
Handler Dispatch Tiers (Detailed Example)
Suppose user is in "REGISTRATION" state and sends text "John":
@BotController
public class RegistrationBot {
// Tier 1: State handler — MATCHES (state is "REGISTRATION")
@BotText("John")
@BotChatState("REGISTRATION")
public String handleRegistrationText() {
return "Name saved!";
}
// Would not be reached (Tier 1 matched first)
@BotText("John")
public String handleGeneralText() {
return "Generic response";
}
// Would not be reached (Tier 1 matched first)
@BotTextDefault
public String handleDefault() {
return "Default";
}
}
Result: "Name saved!" is returned (Tier 1 handler executed).
Exception / Error Flow
When a handler method throws an exception, the framework follows this path:
Handler method throws Exception
BotDispatcher catches the exception
@BotExceptionHandler found in same @BotController?
YES → invoke exception handler method → return response
@BotExceptionHandler found in any @BotControllerAdvice?
YES → invoke advice exception handler → return response
NO handler found → exception propagates up to the BotFilter chain
Unhandled exception is logged; update processing ends silently
(no message sent to user)
Always define a catch-all @BotExceptionHandler(Exception.class) in a @BotControllerAdvice
so users receive a friendly error message instead of silence.
@BotControllerAdvice
public class GlobalErrorHandler {
@BotExceptionHandler(Exception.class)
public String onAnyError(Exception ex, User user) {
log.error("Unhandled error for user {}", user.getId(), ex);
return " Something went wrong. Please try again later.";
}
}
Extension Points
1. Custom BotFilter
Intercept all updates for middleware logic (auth, logging, rate-limiting).
2. Custom BotArgumentResolver
Inject custom types into handler methods.
3. Custom BotReturnTypeHandler
Support new return types beyond built-in ones.
4. Custom Transport
Subclass Bot for a new update source.
5. Custom BotChatStateService
Replace in-memory implementation with Redis, JDBC, etc.
Configuration
Required Property
easygram:
token: YOUR_BOT_TOKEN
Optional Properties
# Update delivery transport (defaults to LONG_POLLING — omit block entirely for long-polling bots)
easygram:
update:
transport: LONG_POLLING # LONG_POLLING | WEBHOOK | KAFKA_CONSUMER | RABBIT_CONSUMER | NONE
webhook: # only needed when transport: WEBHOOK
url: https://example.com/webhook
path: /webhook
secret-token: ${WEBHOOK_SECRET}
# Internationalisation (core-i18n module)
easygram:
i18n:
default-locale: en
# Broker integration (messaging-api module — omit block for standalone bots)
# Producer pattern: poll/webhook + publish to broker
easygram:
messaging:
forward-only: false # true = skip local handlers, publish only
fail-on-publish-error: false
producer:
type: KAFKA # KAFKA | RABBIT
kafka:
topic: easygram-updates
group-id: easygram-bot # consumer group ID (KAFKA_CONSUMER transport)
rabbit:
exchange: easygram-exchange
queue: easygram-updates
routing-key: easygram.updates
# Consumer pattern: set transport to receive from broker
# easygram.update.transport: KAFKA_CONSUMER # or RABBIT_CONSUMER
Request/Response Model
BotRequest
Update update // Raw Telegram update
TelegramClient telegramClient // Telegram API client
User user // Resolved sender (set by BotContextSetterFilter)
Chat chat // Resolved chat (set by BotContextSetterFilter)
Throwable throwable // Populated when routing to @BotExceptionHandler
BotMetadata botMetadata // Bot's own info (token, id, username)
// Request-scoped attribute store — share data between filters, resolvers, and handlers:
void setAttribute(String key, Object value) // null removes the key
<T> T getAttribute(String key)
Map<String,Object> getAttributes() // unmodifiable view
BotResponse
// Accumulates API calls to execute; methods:
void addBotApiMethod(BotApiMethod<?> method)
void addBotApiMethods(Collection<BotApiMethod<?>> methods)
Collection<BotApiMethod<?>> getBotApiMethods()
// Response-scoped attribute store (shared with filters and return-type handlers):
void setAttribute(String key, Object value) // null removes the key
<T> T getAttribute(String key)
Map<String,Object> getAttributes() // unmodifiable
Concurrency Model
- Thread-safe — All framework components are thread-safe
- Filter chain — Executed serially per update
- Bot handler registry — Shared and immutable after startup
- Chat state service — Pluggable (in-memory default is thread-safe)
- Argument resolvers — Must be thread-safe
- ExecutorService — Optional batch processing for updates
Dependency Injection
All framework components use Spring dependency injection:
- Auto-wired via
@Autowiredor constructor injection @ConditionalOnMissingBeanallows easy bean overrides- Custom filters, resolvers, and handlers are auto-scanned and registered