Production-ready | Clean Architecture | Multi-Module | Test-Driven
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.
┌─────────────────────────────────────────────┐
│ 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 │
└───────────────────────────────────────────┘
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)
- 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
- Testcontainers (Integration testing)
- 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
- Spring Actuator / Micrometer / Prometheus
- Kotlin Logging / Logback
- Sentry
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-core
provides 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:
// CVE-2025-48924 fix: Force commons-lang3 version globally
configurations.all {
resolutionStrategy.eachDependency {
if (requested.group == "org.apache.commons" && requested.name == "commons-lang3") {
useVersion("3.18.0")
because("CVE-2025-48924 - Fix Uncontrolled Recursion vulnerability")
}
}
}
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 }
}
Output Port (Domain Layer):
// demo-domain/src/main/kotlin/com/example/demo/user/port/UserPort.kt
interface UserPort : UserCommandPort, UserQueryPort
Output Adapter (Infrastructure Layer):
// demo-infrastructure/src/main/kotlin/com/example/demo/persistence/user/adapter/UserRepositoryAdapter.kt
@Repository
class UserRepositoryAdapter(
private val jpaRepository: UserJpaRepository,
private val userMapper: UserMapper
) : UserPort {
override fun findOneById(userId: Long): User? =
jpaRepository.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
fun createUser(@RequestBody @Valid createUserRequest: CreateUserRequest): UserResponse {
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-secret-local.yml # Local secrets
├── application-secret-dev.yml # Development secrets
└── application-secret-prod.yml # Production secrets
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" # Management
Environment 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.yml
andapplication-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:
- Configuration: Grafana & Prometheus
- Actuator Settings: application-common.yml
Note: Replace
{ip address}:8085
with your actual IP address for proper metrics collection
- Complete Docker Compose setup for all services
- Detailed setup guide: Docker Setup Guide
- Java 21 or higher
- Docker & Docker Compose (for infrastructure services)
- Gradle 8.10 (wrapper included)
cd docker && ./setup.sh
For 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:bootRun
# 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 detekt
- API Documentation (Swagger): http://localhost:8085/swagger-ui/index.html
- H2 Console (local environment): http://localhost:8085/h2-console
- Application Server: http://localhost:8085
- MailHog (Email Testing): http://localhost:8025
- PgAdmin (PostgreSQL Management): http://localhost:8088
- Kafka UI (Kafka Management): http://localhost:9000
- Redis: localhost:6379 (CLI/Client access)
- PostgreSQL: localhost:5432 (Database connection)
- Kafka: localhost:9092 (Broker connection)
- Zookeeper: localhost:2181 (Coordination service)
- Grafana (Metrics Dashboard): http://localhost:3000
- Prometheus (Metrics Collection): http://localhost:9090
Hyunwoo Park