| name | spring-boot-kotlin |
| description | Spring Boot with Kotlin framework guardrails, patterns, and best practices.
Use when working with Spring Boot Kotlin projects, or when the user mentions Spring Boot with Kotlin.
Provides coroutine patterns, WebFlux, JPA, security, and idiomatic Kotlin-Spring guidelines.
|
| license | MIT |
| metadata | {"author":"samuel","version":"1.0","category":"framework","language":"kotlin","extensions":".kt"} |
Spring Boot (Kotlin) Framework Guide
Applies to: Spring Boot 3.x, Kotlin 1.9+, REST APIs, Microservices
Use with: .claude/skills/kotlin-guide/SKILL.md for base Kotlin patterns
When to Use
- Enterprise Applications: Proven enterprise-grade framework with vast ecosystem
- Microservices: Excellent with Spring Cloud for distributed systems
- Existing Spring Ecosystem: Leverage Spring libraries, security, data, cloud
- Complex Business Logic: Rich feature set for complex domains
- Team Familiarity: Teams with Java/Spring background transitioning to Kotlin
When NOT to Use
- Lightweight Services: Consider Ktor for simpler, minimal-dependency services
- Mobile Backend: Ktor is more lightweight for mobile-only backends
- Minimal Footprint: Spring Boot has a larger memory and startup footprint
Project Structure
myproject/
├── build.gradle.kts
├── settings.gradle.kts
├── src/
│ ├── main/
│ │ ├── kotlin/com/example/myproject/
│ │ │ ├── MyProjectApplication.kt
│ │ │ ├── config/
│ │ │ │ ├── SecurityConfig.kt
│ │ │ │ ├── WebConfig.kt
│ │ │ │ └── JwtProperties.kt
│ │ │ ├── controller/
│ │ │ │ └── UserController.kt
│ │ │ ├── service/
│ │ │ │ └── UserService.kt
│ │ │ ├── repository/
│ │ │ │ └── UserRepository.kt
│ │ │ ├── model/
│ │ │ │ ├── entity/User.kt
│ │ │ │ └── dto/UserDto.kt
│ │ │ ├── exception/
│ │ │ │ ├── GlobalExceptionHandler.kt
│ │ │ │ └── Exceptions.kt
│ │ │ └── security/
│ │ │ ├── JwtService.kt
│ │ │ ├── JwtAuthenticationFilter.kt
│ │ │ └── CurrentUser.kt
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── application-dev.yml
│ │ └── db/migration/
│ └── test/kotlin/com/example/myproject/
│ ├── controller/
│ ├── service/
│ └── integration/
└── README.md
Guardrails
Spring-Kotlin Specific
- Use constructor injection (Kotlin default, no
@Autowired needed)
- Never use
lateinit var for dependencies -- constructor injection is idiomatic
- Use
@Transactional(readOnly = true) for all read-only operations
- Use
@Valid on all request body parameters for automatic validation
- Use Spring profiles (
application-{profile}.yml) for environment config
- Never hardcode secrets in config files -- use
${ENV_VAR:default} syntax
- Use
@ConfigurationProperties data classes for type-safe configuration
- Prefer Kotlin data classes for DTOs with
@field: annotation target for validation
- Use
open modifier or plugin.spring (auto-opens @Component classes)
Entity Design
- Use
data class for JPA entities with plugin.jpa (auto-generates no-arg constructors)
- Prefer
val for entity properties, use copy() for updates
- Use UUID or auto-generated IDs; nullable
val id: UUID? = null for new entities
- Use
@CreationTimestamp and @UpdateTimestamp for audit fields
- Keep entities in
model/entity/, DTOs in model/dto/
Service Layer
- Mark all service classes with
@Service
- Use
@Transactional for write operations, @Transactional(readOnly = true) for reads
- Throw domain-specific exceptions (
NotFoundException, ConflictException)
- Return DTOs from services, never expose entities to controllers
- Use Kotlin null safety: repository methods return
T? for nullable finds
Controller Layer
- Use
@RestController with @RequestMapping for base paths
- Return
ResponseEntity<T> with explicit status codes
- Use
@Valid @RequestBody for all incoming request bodies
- Use
@PreAuthorize for method-level security
- Use
@PageableDefault for pagination parameters
Key Patterns
Application Entry Point
@SpringBootApplication
class MyProjectApplication
fun main(args: Array<String>) {
runApplication<MyProjectApplication>(*args)
}
Type-Safe Configuration
@ConfigurationProperties(prefix = "jwt")
data class JwtProperties(
val secret: String,
val expiration: Long,
)
jwt:
secret: ${JWT_SECRET:your-256-bit-secret-key-here}
expiration: 86400000
Entity with Data Class
@Entity
@Table(name = "users")
data class User(
@Id
@GeneratedValue(strategy = GenerationType.UUID)
val id: UUID? = null,
@Column(unique = true, nullable = false)
val email: String,
@Column(nullable = false)
val passwordHash: String,
@Column(nullable = false)
val name: String,
@Column(nullable = false)
val role: String = "USER",
@CreationTimestamp
@Column(updatable = false)
val createdAt: Instant? = null,
@UpdateTimestamp
val updatedAt: Instant? = null,
)
DTO with Validation
data class CreateUserRequest(
@field:NotBlank(message = "Email is required")
@field:Email(message = "Invalid email format")
val email: String,
@field:NotBlank(message = "Password is required")
@field:Size(min = 8, message = "Password must be at least 8 characters")
val password: String,
@field:NotBlank(message = "Name is required")
@field:Size(min = 1, max = 100)
val name: String,
)
data class UserResponse(
val id: UUID,
val email: String,
val name: String,
val role: String,
val createdAt: Instant,
) {
companion object {
fun from(user: User): UserResponse = UserResponse(
id = user.id!!,
email = user.email,
name = user.name,
role = user.role,
createdAt = user.createdAt!!,
)
}
}
Repository
@Repository
interface UserRepository : JpaRepository<User, UUID> {
fun findByEmail(email: String): User?
fun existsByEmail(email: String): Boolean
}
Service
@Service
class UserService(
private val userRepository: UserRepository,
private val passwordEncoder: PasswordEncoder,
) {
@Transactional
fun createUser(request: CreateUserRequest): UserResponse {
if (userRepository.existsByEmail(request.email)) {
throw ConflictException("Email already registered")
}
val user = User(
email = request.email,
passwordHash = passwordEncoder.encode(request.password),
name = request.name,
)
return UserResponse.from(userRepository.save(user))
}
@Transactional(readOnly = true)
fun getUserById(id: UUID): UserResponse {
val user = userRepository.findById(id)
.orElseThrow { NotFoundException("User not found: $id") }
return UserResponse.from(user)
}
@Transactional
fun updateUser(id: UUID, request: UpdateUserRequest): UserResponse {
val user = userRepository.findById(id)
.orElseThrow { NotFoundException("User not found: $id") }
val updated = user.copy(
name = request.name ?: user.name,
email = request.email ?: user.email,
)
return UserResponse.from(userRepository.save(updated))
}
}
Controller
@RestController
@RequestMapping("/api")
class UserController(
private val userService: UserService,
) {
@PostMapping("/register")
fun register(
@Valid @RequestBody request: CreateUserRequest,
): ResponseEntity<UserResponse> {
val user = userService.createUser(request)
return ResponseEntity.status(HttpStatus.CREATED).body(user)
}
@GetMapping("/users/{id}")
@PreAuthorize("hasRole('ADMIN') or @userSecurity.isOwner(#id, authentication)")
fun getUser(@PathVariable id: UUID): ResponseEntity<UserResponse> {
return ResponseEntity.ok(userService.getUserById(id))
}
@GetMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
fun getAllUsers(
@PageableDefault(size = 20) pageable: Pageable,
): ResponseEntity<Page<UserResponse>> {
return ResponseEntity.ok(userService.getAllUsers(pageable))
}
}
Exception Handling
class NotFoundException(message: String) : RuntimeException(message)
class UnauthorizedException(message: String) : RuntimeException(message)
class ConflictException(message: String) : RuntimeException(message)
data class ErrorResponse(
val timestamp: Instant = Instant.now(),
val status: Int,
val error: String,
val message: String,
val details: List<String>? = null,
)
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException::class)
fun handleNotFound(ex: NotFoundException) = ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse(status = 404, error = "NOT_FOUND", message = ex.message ?: "Not found"))
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleValidation(ex: MethodArgumentNotValidException): ResponseEntity<ErrorResponse> {
val details = ex.bindingResult.fieldErrors.map { "${it.field}: ${it.defaultMessage}" }
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ErrorResponse(status = 400, error = "VALIDATION_ERROR", message = "Validation failed", details = details))
}
}
Security Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class SecurityConfig(
private val jwtAuthenticationFilter: JwtAuthenticationFilter,
) {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain = http
.csrf { it.disable() }
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.authorizeHttpRequests { auth ->
auth
.requestMatchers("/api/register", "/api/login").permitAll()
.requestMatchers("/health/**", "/actuator/**").permitAll()
.anyRequest().authenticated()
}
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
.build()
@Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder(12)
}
Custom Annotation for Current User
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@AuthenticationPrincipal
annotation class CurrentUser
Coroutines Integration
Spring Boot supports Kotlin coroutines in controllers and services. Add these dependencies:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
Suspend functions in controllers are automatically handled by Spring WebFlux:
@GetMapping("/users/{id}")
suspend fun getUser(@PathVariable id: UUID): ResponseEntity<UserResponse> {
val user = userService.getUserById(id)
return ResponseEntity.ok(user)
}
For detailed coroutine, WebFlux, testing, and advanced security patterns, see references/patterns.md.
Commands
./gradlew bootRun
./gradlew build
./gradlew test
./gradlew test jacocoTestReport
./gradlew ktlintFormat
./gradlew ktlintCheck
./gradlew bootJar
java -jar build/libs/myproject-0.0.1-SNAPSHOT.jar
docker build -t myproject .
Best Practices
DO
- Use data classes for DTOs and entities
- Use Kotlin null safety features (
?., ?:, requireNotNull)
- Use
@Transactional(readOnly = true) for read operations
- Use constructor injection (default in Kotlin)
- Use
@Valid for request validation
- Use Spring profiles for environment configuration
- Use
@field: annotation target for validation annotations on data class properties
- Use coroutines for async operations where beneficial
DON'T
- Use
lateinit var for dependencies (use constructor injection)
- Ignore null safety in Spring Data repositories
- Use
!! operator without proper null checks
- Mix reactive and blocking code in the same call chain
- Store secrets in configuration files
- Expose JPA entities directly in controller responses
References
For detailed patterns and examples, see:
- references/patterns.md -- WebFlux reactive patterns, JPA advanced patterns, testing, security advanced patterns
External References