Spring Boot (Kotlin) Framework Guide
Applies to: Spring Boot 3.x, Kotlin 1.9+, REST APIs, Microservices Use with:
.claude/skills/kotlin-guide/SKILL.mdfor 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
code
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
@Autowiredneeded) - •Never use
lateinit varfor dependencies -- constructor injection is idiomatic - •Use
@Transactional(readOnly = true)for all read-only operations - •Use
@Validon 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
@ConfigurationPropertiesdata classes for type-safe configuration - •Prefer Kotlin data classes for DTOs with
@field:annotation target for validation - •Use
openmodifier orplugin.spring(auto-opens@Componentclasses)
Entity Design
- •Use
data classfor JPA entities withplugin.jpa(auto-generates no-arg constructors) - •Prefer
valfor entity properties, usecopy()for updates - •Use UUID or auto-generated IDs; nullable
val id: UUID? = nullfor new entities - •Use
@CreationTimestampand@UpdateTimestampfor audit fields - •Keep entities in
model/entity/, DTOs inmodel/dto/
Service Layer
- •Mark all service classes with
@Service - •Use
@Transactionalfor 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
@RestControllerwith@RequestMappingfor base paths - •Return
ResponseEntity<T>with explicit status codes - •Use
@Valid @RequestBodyfor all incoming request bodies - •Use
@PreAuthorizefor method-level security - •Use
@PageableDefaultfor pagination parameters
Key Patterns
Application Entry Point
kotlin
@SpringBootApplication
class MyProjectApplication
fun main(args: Array<String>) {
runApplication<MyProjectApplication>(*args)
}
Type-Safe Configuration
kotlin
@ConfigurationProperties(prefix = "jwt")
data class JwtProperties(
val secret: String,
val expiration: Long,
)
yaml
# application.yml
jwt:
secret: ${JWT_SECRET:your-256-bit-secret-key-here}
expiration: 86400000
Entity with Data Class
kotlin
@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
kotlin
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
kotlin
@Repository
interface UserRepository : JpaRepository<User, UUID> {
fun findByEmail(email: String): User?
fun existsByEmail(email: String): Boolean
}
Service
kotlin
@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
kotlin
@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
kotlin
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
kotlin
@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
kotlin
@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:
kotlin
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
Suspend functions in controllers are automatically handled by Spring WebFlux:
kotlin
@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
bash
# Development ./gradlew bootRun # Build ./gradlew build # Test ./gradlew test # Test with coverage ./gradlew test jacocoTestReport # Format (with ktlint) ./gradlew ktlintFormat # Lint ./gradlew ktlintCheck # Build JAR ./gradlew bootJar # Run JAR java -jar build/libs/myproject-0.0.1-SNAPSHOT.jar # Docker build 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
@Validfor 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 varfor 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