Skip to main content

Development Guide

This guide helps you set up a local development environment for the TelegramBot API project and understand its architecture and development workflows.

Prerequisites

Before starting, ensure you have the following installed:
1

Java Development Kit (JDK)

Version: JDK 21 or higher
java -version
The project uses Java 21 features. Earlier versions won’t work.
2

Maven

Version: Maven 3.9 or higher
mvn -version
Maven wrapper (mvnw) is included in the project, so local Maven installation is optional.
3

Docker & Docker Compose

Required for running the PostgreSQL database and full application stack.
docker --version
docker compose version
4

IDE (Optional but Recommended)

Recommended: IntelliJ IDEA (Community or Ultimate)Alternatives: Eclipse, VS Code with Java extensions

Project Setup

Clone the Repository

git clone https://github.com/acamus79/TlgrmBot.git
cd TlgrmBot

Configure Environment Variables

1

Copy environment template

cp .env.example .env
2

Edit .env file

Open .env and configure the required variables:
.env
# Database
DB_USER=postgres
DB_PASSWORD=your_password

# Telegram Bot
TELEGRAM_BOT_TOKEN=your_telegram_token

# OpenRouter AI
OPENROUTE_KEY=your_openrouter_key

# JWT Security
JWT_SECRET=$(openssl rand -base64 32)

# Optional: AI Configuration
AI_SYSTEM_PROMPT="Your custom prompt"
AI_TEMPERATURE=1.2
AI_MAX_TOKENS=150
See the Configuration Guide for detailed instructions.

Running the Application

The easiest way to run the complete stack:
# Build and start all services
docker-compose up --build

# Run in detached mode
docker-compose up -d

# View logs
docker-compose logs -f

# Stop all services
docker-compose down
The application will be available at:
Docker Compose automatically sets up:
  • PostgreSQL database container
  • Application container with all dependencies
  • Network configuration
  • Volume persistence for database data

Option 2: Local Development (Without Docker)

For active development with faster rebuild times:
1

Start PostgreSQL

You need a local PostgreSQL instance:
docker run -d \
  --name telegrmdb \
  -e POSTGRES_USER=postgres \
  -e POSTGRES_PASSWORD=secret_password \
  -e POSTGRES_DB=telegrmdb \
  -p 5432:5432 \
  postgres:15
2

Configure database URL

Add to your .env:
DB_URL=jdbc:postgresql://localhost:5432/telegrmdb
3

Build the project

mvn clean install
Or using the wrapper:
./mvnw clean install
4

Run the application

mvn spring-boot:run
Hot Reload: Spring Boot DevTools is included in pom.xml:55-58. Changes to classes will trigger automatic restart.

Running Tests

The project includes comprehensive unit and integration tests.

Run All Tests

mvn clean verify
Or with the wrapper:
./mvnw clean verify
Testcontainers: Integration tests use Testcontainers to spin up a PostgreSQL instance automatically. Docker must be running.

Run Specific Test Classes

# Run a specific test class
mvn test -Dtest=RegisterUserUseCaseTest

# Run tests matching a pattern
mvn test -Dtest=*UseCaseTest

Test Structure

The test suite includes:
Located in src/test/java/com/acamus/telegrm/application/usecases/
  • RegisterUserUseCaseTest - User registration logic
  • ProcessTelegramUpdateUseCaseTest - Telegram message processing
  • SendMessageUseCaseTest - Message sending logic
Characteristics:
  • Fast execution
  • No external dependencies
  • Uses Mockito for mocking
Located in src/test/java/com/acamus/telegrm/infrastructure/adapters/
  • ConversationRepositoryAdapterTest - Database operations
  • TelegrmApplicationTests - Full application context
Characteristics:
  • Tests real database interactions
  • Uses Testcontainers for PostgreSQL
  • Extends BaseIntegrationTest

Base Integration Test

src/test/java/com/acamus/telegrm/BaseIntegrationTest.java
@SpringBootTest
@Testcontainers
public abstract class BaseIntegrationTest {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");
    
    // Configuration for tests
}
Integration tests take longer to run because they start a real PostgreSQL container.

Project Structure

The project follows Hexagonal Architecture (Ports and Adapters):
src/main/java/com/acamus/telegrm/
├── core/                           # Domain Layer (Business Logic)
│   ├── domain/
│   │   ├── model/                  # Domain Entities
│   │   │   ├── User.java
│   │   │   ├── Conversation.java
│   │   │   ├── Message.java
│   │   │   └── TelegramChat.java
│   │   ├── valueobjects/           # Value Objects
│   │   │   ├── Email.java
│   │   │   ├── Password.java
│   │   │   ├── MessageContent.java
│   │   │   └── TelegramChatId.java
│   │   └── exception/              # Domain Exceptions
│   │       ├── EmailAlreadyExistsException.java
│   │       └── InvalidCredentialsException.java
│   └── ports/                      # Port Interfaces
│       ├── in/                     # Input Ports (Use Cases)
│       │   ├── user/
│       │   │   ├── RegisterUserPort.java
│       │   │   ├── AuthenticateUserPort.java
│       │   │   └── *Command.java
│       │   ├── conversation/
│       │   │   ├── SendMessagePort.java
│       │   │   ├── ListConversationsPort.java
│       │   │   └── GetMessagesByConversationPort.java
│       │   └── telegram/
│       │       └── ProcessTelegramUpdatePort.java
│       └── out/                    # Output Ports (Dependencies)
│           ├── user/
│           │   └── UserRepositoryPort.java
│           ├── conversation/
│           │   ├── ConversationRepositoryPort.java
│           │   └── MessageRepositoryPort.java
│           ├── ai/
│           │   └── AiGeneratorPort.java
│           ├── telegram/
│           │   └── TelegramPort.java
│           └── security/
│               └── TokenGeneratorPort.java

├── application/                    # Application Layer (Use Cases)
│   └── usecases/
│       ├── user/
│       │   ├── RegisterUserUseCase.java
│       │   └── AuthenticateUserUseCase.java
│       ├── conversation/
│       │   ├── SendMessageUseCase.java
│       │   ├── ListConversationsUseCase.java
│       │   └── GetMessagesByConversationUseCase.java
│       └── telegram/
│           └── ProcessTelegramUpdateUseCase.java

├── infrastructure/                 # Infrastructure Layer (Adapters)
│   ├── adapters/
│   │   ├── in/                     # Input Adapters
│   │   │   ├── web/                # REST Controllers
│   │   │   │   ├── auth/
│   │   │   │   │   └── AuthController.java
│   │   │   │   └── conversation/
│   │   │   │       └── ConversationController.java
│   │   │   └── scheduler/
│   │   │       └── TelegramPollingService.java
│   │   └── out/                    # Output Adapters
│   │       ├── persistence/        # Database
│   │       │   ├── jpa/
│   │       │   │   ├── entities/
│   │       │   │   └── repositories/
│   │       │   ├── UserRepositoryAdapter.java
│   │       │   ├── ConversationRepositoryAdapter.java
│   │       │   └── MessageRepositoryAdapter.java
│   │       ├── telegram/           # Telegram API
│   │       │   └── TelegramAdapter.java
│   │       ├── ai/                 # OpenRouter AI
│   │       │   └── OpenRouterAdapter.java
│   │       └── security/           # JWT & Password
│   │           ├── JwtTokenProvider.java
│   │           ├── PasswordHasher.java
│   │           └── CustomUserDetailsService.java
│   ├── config/                     # Spring Configuration
│   │   ├── BeanConfiguration.java
│   │   ├── SecurityConfig.java
│   │   ├── JwtAuthenticationFilter.java
│   │   ├── OpenRouterConfig.java
│   │   ├── TelegramConfig.java
│   │   └── OpenApiConfig.java
│   ├── decorators/                 # Transaction Management
│   │   ├── TransactionalSendMessageUseCase.java
│   │   └── TransactionalProcessTelegramUpdateUseCase.java
│   └── exceptions/                 # Global Exception Handling
│       └── GlobalExceptionHandler.java

└── TelegrmApplication.java         # Main Application Class

Key Architectural Principles

The core/ package contains pure business logic with no framework dependencies.
  • No Spring annotations in domain code
  • No JPA entities in the domain
  • Domain defines interfaces (ports), infrastructure implements them
This makes the domain easy to test and completely portable.
Dependencies flow inward:
Infrastructure → Application → Domain
  • Domain depends on nothing
  • Application depends on domain
  • Infrastructure depends on both
  • Ports: Interfaces defined in the domain
  • Adapters: Implementations in infrastructure
Example:
// Port (in core/ports/out/)
public interface AiGeneratorPort {
    String generateResponse(String input);
}

// Adapter (in infrastructure/adapters/out/ai/)
@Component
public class OpenRouterAdapter implements AiGeneratorPort {
    // Implementation using OpenRouter API
}

Database Migrations

The project uses Flyway for database version control.

Migration Files

Located in src/main/resources/db/migration/:
db/migration/
├── V1__create_users_table.sql
├── V2__create_conversations_table.sql
└── V3__create_messages_table.sql

Creating a New Migration

1

Create migration file

Follow the naming convention: V{version}__{description}.sql
touch src/main/resources/db/migration/V4__add_user_roles.sql
2

Write SQL

V4__add_user_roles.sql
ALTER TABLE users ADD COLUMN role VARCHAR(50) DEFAULT 'USER';
CREATE INDEX idx_users_role ON users(role);
3

Restart application

Flyway automatically applies pending migrations on startup.
docker-compose restart
Never modify existing migration files after they’ve been applied. Always create a new migration for changes.

Adding New Features

Follow the hexagonal architecture pattern:
1

Define domain model

Add entities and value objects in core/domain/model/
public class Feature {
    private final String id;
    private final String name;
    // Domain logic here
}
2

Create ports

Define input port (use case interface) in core/ports/in/:
public interface CreateFeaturePort {
    Feature create(CreateFeatureCommand command);
}
Define output port (dependency interface) in core/ports/out/:
public interface FeatureRepositoryPort {
    Feature save(Feature feature);
    Optional<Feature> findById(String id);
}
3

Implement use case

Create use case in application/usecases/:
public class CreateFeatureUseCase implements CreateFeaturePort {
    private final FeatureRepositoryPort repository;
    
    @Override
    public Feature create(CreateFeatureCommand command) {
        // Business logic
        return repository.save(feature);
    }
}
4

Implement adapters

Input adapter (REST controller):
@RestController
@RequestMapping("/features")
public class FeatureController {
    private final CreateFeaturePort createFeaturePort;
    
    @PostMapping
    public ResponseEntity<FeatureDto> create(@RequestBody CreateFeatureRequest request) {
        Feature feature = createFeaturePort.create(new CreateFeatureCommand(request.name()));
        return ResponseEntity.ok(toDto(feature));
    }
}
Output adapter (repository):
@Component
public class FeatureRepositoryAdapter implements FeatureRepositoryPort {
    private final FeatureJpaRepository jpaRepository;
    
    @Override
    public Feature save(Feature feature) {
        FeatureEntity entity = toEntity(feature);
        FeatureEntity saved = jpaRepository.save(entity);
        return toDomain(saved);
    }
}
5

Configure beans

Wire everything in BeanConfiguration.java:
@Bean
public CreateFeaturePort createFeatureUseCase(FeatureRepositoryPort repository) {
    return new CreateFeatureUseCase(repository);
}
6

Write tests

Unit test for use case:
class CreateFeatureUseCaseTest {
    @Test
    void shouldCreateFeature() {
        // Arrange
        FeatureRepositoryPort repository = mock(FeatureRepositoryPort.class);
        CreateFeatureUseCase useCase = new CreateFeatureUseCase(repository);
        
        // Act
        Feature result = useCase.create(new CreateFeatureCommand("New Feature"));
        
        // Assert
        verify(repository).save(any(Feature.class));
    }
}

Development Tools

Swagger UI

Interactive API documentation at: http://localhost:8080/swagger-ui.html
Configure API documentation in OpenApiConfig.java. All controllers are automatically documented using OpenAPI annotations.

Bruno Collection

A pre-configured Bruno collection is included in the project:
# Open in Bruno
open Bruno/
Alternatively, import via the Bruno button in the README.
Bruno is a Git-friendly alternative to Postman, perfect for team collaboration.

Database Tools

Docker:
docker exec -it $(docker ps -qf "name=postgres") psql -U postgres -d telegrmdb
GUI Tools:
  • DBeaver: Universal database client
  • pgAdmin: PostgreSQL-specific
  • IntelliJ Database Tools: Built into IntelliJ IDEA Ultimate
Connection Details:
  • Host: localhost
  • Port: 5432
  • Database: telegrmdb
  • User: postgres
  • Password: (from your .env)

Troubleshooting

Another application is using port 8080.Solution:
# Find process using port 8080
lsof -i :8080

# Kill the process
kill -9 <PID>

# Or change the port in application.yaml
server:
  port: 8081
Flyway detected a checksum mismatch or failed migration.Solution:
# Reset database
docker-compose down -v
docker-compose up --build
This deletes all data. Use only in development.
Testcontainers requires Docker to be running.Solution:
# Start Docker Desktop or Docker daemon
# Then run tests again
mvn test
IDE hasn’t recognized Lombok-generated code.Solution (IntelliJ):
  1. Install Lombok plugin
  2. Enable annotation processing:
    • Settings → Build → Compiler → Annotation Processors
    • Check “Enable annotation processing”
  3. Rebuild project

Useful Commands

# Clean build
mvn clean install -DskipTests

# Run with specific profile
mvn spring-boot:run -Dspring-boot.run.profiles=dev

# Generate JAR without running tests
mvn package -DskipTests

# Check for dependency updates
mvn versions:display-dependency-updates

# Format code (if configured)
mvn spotless:apply

# View dependency tree
mvn dependency:tree

Next Steps

Architecture Deep Dive

Learn more about hexagonal architecture

API Reference

Explore all available endpoints

Configuration

Review all configuration options

Authentication

Understand the JWT authentication flow

Build docs developers (and LLMs) love