Kotlin Spring Boot Patterns Project Configuration // build.gradle.kts plugins { kotlin("jvm") version "2.2.21" kotlin("plugin.spring") version "2.2.21" id("org.springframework.boot") version "3.5.7" }
dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-data-jdbc") implementation("org.springframework.boot:spring-boot-starter-validation") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") }
Entity Pattern data class Environment( val id: UUID, val name: String, val status: EnvironmentStatus, val createdAt: Instant, val updatedAt: Instant? )
enum class EnvironmentStatus { PENDING, RUNNING, STOPPED, FAILED }
Service Pattern
@Service
class EnvironmentService(
private val repository: EnvironmentRepository,
private val computeClient: ComputeClient
) {
// Use NEVER propagation - let caller control transaction
@Transactional(propagation = Propagation.NEVER)
fun create(request: CreateEnvironmentRequest): Pair
// Create new
val environment = Environment(
id = UUID.randomUUID(),
name = request.name,
status = EnvironmentStatus.PENDING,
createdAt = Instant.now(),
updatedAt = null
)
val saved = repository.save(environment)
return Pair(saved.toResponse(), true) // created
}
fun findById(id: UUID): Environment =
repository.findById(id)
?: throw ResourceNotFoundRestException("Environment", id)
fun findAll(): List<Environment> =
repository.findAll()
}
Controller Pattern @RestController class EnvironmentController( private val service: EnvironmentService ) : EnvironmentApi {
override fun create(request: CreateEnvironmentRequest): ResponseEntity<EnvironmentResponse> {
val (result, isNew) = service.create(request)
return if (isNew) {
ResponseEntity.status(HttpStatus.CREATED).body(result)
} else {
ResponseEntity.ok(result)
}
}
override fun getById(id: UUID): ResponseEntity<EnvironmentResponse> =
ResponseEntity.ok(service.findById(id).toResponse())
override fun list(): ResponseEntity<List<EnvironmentResponse>> =
ResponseEntity.ok(service.findAll().map { it.toResponse() })
}
API Interface Pattern (OpenAPI) @Tag(name = "Environments", description = "Environment management") interface EnvironmentApi {
@Operation(summary = "Create environment")
@ApiResponses(
ApiResponse(responseCode = "201", description = "Created"),
ApiResponse(responseCode = "200", description = "Already exists"),
ApiResponse(responseCode = "400", description = "Validation error")
)
@PostMapping("/api/v1/environments")
fun create(
@RequestBody @Valid request: CreateEnvironmentRequest
): ResponseEntity<EnvironmentResponse>
@Operation(summary = "Get environment by ID")
@GetMapping("/api/v1/environments/{id}")
fun getById(@PathVariable id: UUID): ResponseEntity<EnvironmentResponse>
@Operation(summary = "List all environments")
@GetMapping("/api/v1/environments")
fun list(): ResponseEntity<List<EnvironmentResponse>>
}
DTO Pattern data class CreateEnvironmentRequest( @field:NotBlank(message = "Name is required") @field:Size(max = 100, message = "Name must be <= 100 chars") val name: String,
@field:Size(max = 500)
val description: String? = null
)
data class EnvironmentResponse( val id: UUID, val name: String, val status: String, val createdAt: Instant )
// Extension function for mapping fun Environment.toResponse() = EnvironmentResponse( id = id, name = name, status = status.name, createdAt = createdAt )
Exception Handling // Typed exceptions throw ResourceNotFoundRestException("Environment", id) throw ValidationRestException("Name cannot be empty") throw ConflictRestException("Environment already exists")
// Global handler @RestControllerAdvice class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundRestException::class)
fun handleNotFound(ex: ResourceNotFoundRestException): ResponseEntity<ErrorResponse> =
ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse(ex.message ?: "Not found"))
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleValidation(ex: MethodArgumentNotValidException): ResponseEntity<ErrorResponse> {
val errors = ex.bindingResult.fieldErrors.map { "${it.field}: ${it.defaultMessage}" }
return ResponseEntity.badRequest()
.body(ErrorResponse("Validation failed", errors))
}
}
Kotlin Idioms // Use ?.let for optional operations user?.let { repository.save(it) }
// Use when for exhaustive matching when (status) { EnvironmentStatus.PENDING -> startEnvironment() EnvironmentStatus.RUNNING -> return // already running EnvironmentStatus.STOPPED -> restartEnvironment() EnvironmentStatus.FAILED -> throw IllegalStateException("Cannot start failed env") }
// Avoid !! - use alternatives repository.findById(id).single() // throws if not exactly one repository.findById(id).firstOrNull() // returns null if none
// Data class copy for immutable updates val updated = environment.copy( status = EnvironmentStatus.RUNNING, updatedAt = Instant.now() )
Configuration Properties @ConfigurationProperties(prefix = "orca") data class OrcaProperties( val compute: ComputeProperties, val timeouts: TimeoutProperties ) { data class ComputeProperties( val url: String, val timeout: Duration = Duration.ofSeconds(30) )
data class TimeoutProperties(
val creation: Duration = Duration.ofMinutes(5),
val termination: Duration = Duration.ofMinutes(2)
)
}