Claude-skill-registry kotlin-spring-boot
Kotlin/Spring Boot 3.x patterns - use for backend services, REST APIs, dependency injection, controllers, and service layers
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/kotlin-spring-boot" ~/.claude/skills/majiayu000-claude-skill-registry-kotlin-spring-boot && rm -rf "$T"
manifest:
skills/data/kotlin-spring-boot/SKILL.mdsource content
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<EnvironmentResponse, Boolean> { // Check for existing (idempotency) repository.findByName(request.name)?.let { return Pair(it.toResponse(), false) // existing } // 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) ) }