Production-ready | Clean Architecture | Multi-Module | Test-Driven | Observability
Original Repository: https://github.com/awakelife93/spring-boot-kotlin-boilerplate
This project is migrated from spring-boot-kotlin-boilerplate to implement Hexagonal Architecture and Multi-Module structure.
A production-ready Spring Boot multi-module project template built with Kotlin. It follows Hexagonal Architecture (Ports and Adapters) principles and Domain-Driven Design (DDD) patterns to ensure maintainability, testability, and scalability. The project includes a complete OpenTelemetry-based observability stack for unified monitoring, distributed tracing, and log aggregation.
- Getting Started
- Architecture Overview
- Key Features
- Technology Stack
- Build Configuration
- Development Commands
- Hexagonal Architecture Implementation
- Configuration Management
- Service Access URLs
- Java 21 or higher
- Docker & Docker Compose (for infrastructure services)
- Gradle 8.10 (wrapper included)
cd docker && ./setup.shFor detailed setup information, see Docker Setup Guide which explains:
- Network configuration and auto-creation
- Individual service management
- Service dependencies and startup order
./gradlew :demo-bootstrap:bootRunApplication is running at http://localhost:8085
See Service Access URLs for all available services.
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ Observability Layer (OpenTelemetry) — Spans All Layers │
│ Metrics | Traces | Logs → │
│ Prometheus / Tempo / Loki │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Driving Adapters (Input) │ │
│ │ demo-internal-api / demo-external-api / demo-batch │ │
│ └──────────────────────────┬──────────────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Input Ports │ │
│ └────────┬────────┘ │
│ │ │
│ ┌──────────────────────────▼──────────────────────────┐ │
│ │ Application Core │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────┐ │ │
│ │ │ demo-application (UseCases) │ │ │
│ │ │ • Business Logic │ │ │
│ │ │ • Orchestration │ │ │
│ │ └──────────────────────┬────────────────────┘ │ │
│ │ │ │ │
│ │ ┌──────────────────────▼────────────────────┐ │ │
│ │ │ demo-domain (Entities) │ │ │
│ │ │ • Domain Models │ │ │
│ │ │ • Business Rules │ │ │
│ │ │ • Port Interfaces │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────┬──────────────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Output Ports │ │
│ └────────┬────────┘ │
│ │ │
│ ┌──────────────────────────▼──────────────────────────┐ │
│ │ Driven Adapters (Output) │ │
│ │ demo-infrastructure │ │
│ │ • Database (JPA/PostgreSQL) │ │
│ │ • Cache (Redis) │ │
│ │ • Message Queue (Kafka) │ │
│ │ • Email Service │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
The following concerns span across all architectural layers:
Observability - Unified monitoring via OpenTelemetry (see Key Features #9)
Security - Spring Security, JWT, RBAC (see Technology Stack section)
Logging - Kotlin Logging, Logback (see Technology Stack section)
Error Handling - Global exception handling, webhook notifications (Slack/Discord), Sentry integration
root/
├── demo-core/ # Core utilities, configurations, and test fixtures
├── demo-domain/ # Domain entities, value objects, and Port Interfaces
├── demo-application/ # Use cases with Input/Output Ports
├── demo-infrastructure/ # Adapters for Output Ports (DB, Cache, MQ, etc.)
├── demo-internal-api/ # Adapters for Input Ports (Internal REST API)
├── demo-external-api/ # Adapters for Input Ports (External/Public API)
├── demo-batch/ # Adapters for Input Ports (Batch processing)
├── demo-bootstrap/ # Application bootstrap and main entry point
├── gradle/ # Gradle configuration and version catalogs
│ └── libs.versions.toml # Centralized dependency version management
├── docker/ # Docker compose configurations
└── monitoring/ # Monitoring configurations (Prometheus, Grafana, OpenTelemetry Collector, Tempo, Loki)
- Language: Kotlin 2.0.21
- Framework: Spring Boot 3.5.5
- JVM: Java 21 LTS
- Spring WebMVC / WebFlux
- Spring Validation
- SpringDoc OpenAPI (Swagger UI)
- Jackson for JSON processing
- WebClient
- Spring Security
- JWT
- BCrypt password encoding
- Role-based access control (RBAC)
- Spring Data JPA
- QueryDSL
- Flyway
- PostgreSQL / H2
- Spring Data Redis
- Apache Kafka (via Spring Kafka)
- Event-driven architecture support
- JUnit 5 / Kotest
- MockK / Mockito Kotlin & Mockito Inline
- Spring Boot Test
- Spring MockMvc
- Ktlint / Detekt (Code quality)
- Spring Boot DevTools (Hot reload)
- Gradle 8.10 with Kotlin DSL
- Gradle Version Catalogs (libs.versions.toml) for centralized dependency management
- Docker & Docker Compose
- MailHog / PgAdmin / Kafka UI
- Observability Stack: OpenTelemetry Collector, Prometheus, Grafana, Tempo, Loki
- Application Monitoring: Spring Actuator, Sentry
- Logging: Kotlin Logging, Logback
The root build.gradle.kts manages all subprojects with shared configurations through subprojects block.
Hexagonal Architecture Module Dependencies:
// demo-core auto-dependency for all modules (except itself)
if (project.name != "demo-core") {
api(project(":demo-core"))
}
// Test fixtures sharing for specific modules
val modulesUsingTestFixtures = listOf(
"demo-application", "demo-infrastructure",
"demo-internal-api", "demo-external-api",
"demo-batch", "demo-domain"
)Test Fixtures Strategy:
demo-coreprovides common test utilities, mock data, and base test classes shared across modules- Each listed module uses these fixtures for their specific testing needs:
demo-application: Use case testingdemo-infrastructure: Repository/adapter testingdemo-internal-api: Controller integration testingdemo-external-api: API endpoint testingdemo-batch: Batch job testingdemo-domain: Domain model testing
- Excluded modules:
demo-bootstrap: Main application module (no test fixtures needed)demo-core: Source of test fixtures (doesn't consume itself)
Security Vulnerability Management:
- All dependency vulnerabilities are centrally managed through the
applyCveFixes()function in the rootbuild.gradle.kts - CVE fixes are automatically applied to all subprojects during dependency resolution
// Example: CVE fix implementation
when {
requested.group == "org.apache.commons" && requested.name == "commons-lang3" -> {
useVersion("3.18.0")
because("CVE-2025-48924")
}
}Executable vs Library Modules:
val executableModules = listOf("demo-bootstrap")
if (project.name !in executableModules) {
// Library modules: disable bootJar, enable regular jar
tasks.named<BootJar>("bootJar") { enabled = false }
tasks.named<Jar>("jar") { enabled = true }
}# Build all modules
./gradlew build
# Run tests
./gradlew test
# Run specific module tests
./gradlew :demo-application:test# Run ktlint check
./gradlew ktlintCheck
# Format code with ktlint
./gradlew ktlintFormat
# Run detekt analysis
./gradlew detektOutput Port (Domain Layer):
// demo-domain/src/main/kotlin/com/example/demo/user/port/UserPort.kt
interface UserPort : UserCommandPort, UserQueryPortOutput Adapter (Infrastructure Layer):
// demo-infrastructure/src/main/kotlin/com/example/demo/persistence/user/adapter/UserRepositoryAdapter.kt
@Repository
class UserRepositoryAdapter(
private val userJpaRepository: UserJpaRepository,
private val userMapper: UserMapper
) : UserPort {
override fun findOneById(userId: Long): User? =
userJpaRepository.findOneById(userId)?.let {
userMapper.toDomain(it)
}
// ... other implementations
}Input (Use Case - Application Layer):
// demo-application/src/main/kotlin/com/example/demo/user/usecase/CreateUserUseCase.kt
@Component
class CreateUserUseCase(
private val userService: UserService
) : UseCase<CreateUserInput, UserOutput.AuthenticatedUserOutput> {
override fun execute(input: CreateUserInput): UserOutput.AuthenticatedUserOutput =
with(input) {
userService.registerNewUser(this)
}
}Input Adapter (REST Controller):
// demo-internal-api/src/main/kotlin/com/example/demo/user/presentation/UserController.kt
@RestController
@RequestMapping("/api/v1/users")
class UserController(
private val createUserUseCase: CreateUserUseCase
) {
@PostMapping("/register")
fun createUser(
@RequestBody @Valid createUserRequest: CreateUserRequest
): ResponseEntity<CreateUserResponse> {
val input = UserPresentationMapper.toCreateUserInput(createUserRequest)
val userOutput = createUserUseCase.execute(input)
val response = UserPresentationMapper.toCreateUserResponse(userOutput)
return ResponseEntity.status(HttpStatus.CREATED).body(response)
}
}The project implements a multi-layered configuration approach that balances modularity with maintainability:
Each module manages its own library-specific configurations:
demo-bootstrap/
└── src/main/resources/
└── application-bootstrap.yml # Sentry configuration
demo-internal-api/
└── src/main/resources/
└── application-internal-api.yml # SpringDoc/Swagger configuration
demo-external-api/
└── src/main/resources/
└── application-external-api.yml # Webhook configuration
demo-infrastructure/
└── src/main/resources/
└── application-infrastructure.yml # Actuator/Management configuration
demo-core/src/main/resources/
├── application-common.yml # Common settings + imports
├── application-dev.yml # Development environment
├── application-prod.yml # Production environment
├── application-local.yml # Local development
├── application-test.yml # Test environment
├── application-secret-local.yml # Local secrets
├── application-secret-dev.yml # Development secrets
└── application-secret-prod.yml # Production secrets
demo-core/src/testFixtures/
├── kotlin/.../demo/
│ ├── TestApplication.kt # Test application entry point
│ └── config/
│ └── KotestSpringConfig.kt # Kotest Spring configuration
└── resources/
└── kotest.properties # Kotest settings (copied to modulesUsingTestFixtures)
Core configuration imports all module-specific settings:
# demo-core/src/main/resources/application-common.yml
spring:
config:
import:
- "optional:classpath:application-bootstrap.yml" # Sentry
- "optional:classpath:application-internal-api.yml" # SpringDoc
- "optional:classpath:application-external-api.yml" # Webhook
- "optional:classpath:application-infrastructure.yml" # ManagementEnvironment files can override any module setting:
# application-prod.yml
springdoc:
swagger-ui:
enabled: false # Override from demo-internal-api module
api-docs:
enabled: false
sentry:
dsn: # Override from demo-bootstrap module (empty for prod)
logging:
minimum-event-level: ERROR
webhook:
slack:
url: # Override from demo-external-api module (empty for prod)Note: IDE may show "Cannot resolve configuration property" warnings for cross-module properties. This is expected and can be ignored as the configuration works correctly at runtime.
Spring Boot loads configurations in this priority order:
- Environment-specific files (
application-prod.yml) - Module-specific files (
application-bootstrap.yml) - Common configuration (
application-common.yml)
This ensures environment settings always take precedence over module defaults.
- DDL Management: Uses Flyway for migration scripts instead of JPA auto-generation
- Migration Scripts: Located in demo-core/src/main/resources/db/migration
- Alternative: JPA DDL auto-generation available via configuration in application-common.yml
- Metadata Tables: Create Spring Batch metadata table for all environments
- PostgreSQL Schema: Uses batch-postgresql-metadata-schema.sql
- Reference: Spring Batch Schema
- Configuration: enable & route endpoint (default enabled)
- Supported Types: Slack, Discord
// Usage examples
webHookProvider.sendAll(
"Subscription request received from method ${parameter.method?.name}.",
mutableListOf("Request Body: $body")
)
webHookProvider.sendSlack(
"Failed to send message to Kafka",
mutableListOf("Error: ${exception.message}")
)
webHookProvider.sendDiscord(
"Batch processing completed",
mutableListOf("Results: $results")
)- MailHog Integration: Email testing tool with SMTP port 1025
- Configuration: Settings in
application-local.ymlandapplication-secret-local.yml
- Ktlint: Official lint rules, configuration in .editorconfig
- Report output:
build/reports/ktlint
- Report output:
- Detekt: Static analysis, rules in detekt.yml
- Report output:
build/reports/detekt
- Report output:
Mockito-based Testing:
Kotest & MockK Testing:
- BaseIntegrationController
- Security Bypass: SecurityListenerFactory
// Example: Bypassing Spring Security in tests
listeners(SecurityListenerFactory())
Then("Call DELETE /api/v1/users/{userId}").config(tags = setOf(SecurityListenerFactory.NonSecurityOption)) {
// ... test implementation
}- Topic Management: KafkaTopicMetaProvider
- DLQ Support: Dynamic DLQ creation with default fallback partition: 1
User Registration Flow:
User Cleanup Flow:
Architecture:
- All observability data (metrics, traces, logs) are collected through OpenTelemetry Collector
- Spring Boot application uses OpenTelemetry Spring Boot Starter (SDK) to auto-instrument and send telemetry data
- OpenTelemetry Collector routes data to Prometheus, Tempo, and Loki
Configuration Files:
- OpenTelemetry Collector: otel-collector-config.yml
- Receives: OTLP gRPC (localhost:4317), OTLP HTTP (localhost:4318)
- Exports to: Prometheus, Tempo, Loki
- Prometheus: prometheus.yml - Metrics collection
- Tempo: tempo.yml - Distributed tracing
- Loki: loki.yml - Log aggregation
- Grafana: Unified dashboard at http://localhost:3000
Application Settings:
- application-infrastructure.yml
- OpenTelemetry exporter configuration (
management.otel.*) - Spring Actuator settings for observability
- OpenTelemetry exporter configuration (
- Complete Docker Compose setup for all services
- Detailed setup guide: Docker Setup Guide
| Service | URL | Description |
|---|---|---|
| API Documentation | http://localhost:8085/swagger-ui/index.html | Swagger UI for API testing |
| H2 Console | http://localhost:8085/h2-console | Database console (local only) |
| Application Server | http://localhost:8085 | Main application endpoint |
| Service | URL | Description |
|---|---|---|
| MailHog | http://localhost:8025 | Email testing interface |
| PgAdmin | http://localhost:8088 | PostgreSQL management |
| Kafka UI | http://localhost:9000 | Kafka topic management |
| Redis | localhost:6379 | Redis CLI/Client access |
| PostgreSQL | localhost:5432 | Database connection |
| Kafka Broker | localhost:9092 | Kafka broker connection |
| Zookeeper | localhost:2181 | Coordination service |
| Service | URL | Credentials | Description |
|---|---|---|---|
| Grafana | http://localhost:3000 | demo / demo | Unified observability dashboard |
| Prometheus | http://localhost:9090 | - | Metrics collection |
| Tempo | http://localhost:3200 | - | Distributed tracing |
| Loki | http://localhost:3100 | - | Log aggregation |
| OTLP Collector (gRPC) | localhost:4317 | - | OpenTelemetry gRPC endpoint |
| OTLP Collector (HTTP) | localhost:4318 | - | OpenTelemetry HTTP endpoint |
Grafana Data Source Configuration (use Docker internal network addresses):
- Prometheus:
http://prometheus:9090 - Tempo:
http://tempo:3200 - Loki:
http://loki:3100
Hyunwoo Park