skills/kotlin-spring-patterns/SKILL.md
Kotlin + Spring Boot 4.x patterns for backend services — use when implementing backend features, writing services, repositories, or controllers. Pair with the `kotlin-spring-boot` skill for current versions and Spring Boot 4 migration notes.
npx skillsauth add andvl1/claude-plugin kotlin-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Versions for Spring Boot, Kotlin, Jackson 3, etc. — see the
kotlin-spring-bootskill. Always defer to that skill's version table; do not encode versions inline here.
data class EntityName(
val id: UUID,
val name: String,
val createdAt: Instant,
val updatedAt: Instant?
)
Default propagation rule:
@Transactional (REQUIRED). Atomicity required; partial writes corrupt state.@Transactional(readOnly = true) or no annotation if single-query.Propagation.NEVER → ONLY for orchestrators that MUST run outside any tx (e.g., outbox publisher delegating to REQUIRES_NEW worker, or method that triggers @Async/external IO and must not hold tx context). Wrong default for write services — silent breakage if caller wraps you in tx.@Service
class EntityNameService(
private val repository: EntityNameRepository,
private val relatedService: RelatedService
) {
@Transactional
fun create(request: CreateRequest): Pair<EntityResponse, Boolean> {
// Write path — REQUIRED tx. Atomic insert + related writes.
// Return Pair for idempotent operations.
}
@Transactional(readOnly = true)
fun findById(id: UUID): EntityName? =
repository.findById(id)
@Transactional(readOnly = true)
fun findAll(): List<EntityName> =
repository.findAll()
// Rare: orchestrator that must NOT participate in caller's tx.
@Transactional(propagation = Propagation.NEVER)
fun publishOutbox(id: UUID) {
outboxWorker.flushOne(id) // worker uses REQUIRES_NEW internally
}
}
Email, audit logs, cascade events, webhook dispatch — run AFTER commit, not inside tx.
Why:
@Async invoked inside tx leaks tx context to executor thread (or sees uncommitted state via separate connection — race).@TransactionalEventListener(phase = AFTER_COMMIT) fires only on successful commit. @Async on listener moves work off request thread.data class EntityCreated(val id: UUID, val name: String)
@Service
class EntityNameService(
private val repository: EntityNameRepository,
private val publisher: ApplicationEventPublisher
) {
@Transactional
fun create(request: CreateRequest): EntityResponse {
val entity = repository.save(request.toEntity())
publisher.publishEvent(EntityCreated(entity.id, entity.name))
return entity.toResponse()
}
}
@Component
class EntityNotificationListener(
private val mailer: Mailer
) {
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun onCreated(event: EntityCreated) {
mailer.sendWelcome(event.id) // failure here cannot rollback the entity
}
}
Enable async: @EnableAsync on a @Configuration class. Define a named TaskExecutor bean for production (default SimpleAsyncTaskExecutor spawns unbounded threads).
@Repository
class EntityNameRepository(
private val dsl: DSLContext
) {
fun findById(id: UUID): EntityName? =
dsl.selectFrom(ENTITY_NAME)
.where(ENTITY_NAME.ID.eq(id))
.fetchOne()
?.toEntity()
fun findAll(): List<EntityName> =
dsl.selectFrom(ENTITY_NAME)
.fetch()
.map { it.toEntity() }
fun save(entity: EntityName): EntityName =
dsl.insertInto(ENTITY_NAME)
.set(ENTITY_NAME.ID, entity.id)
.set(ENTITY_NAME.NAME, entity.name)
.set(ENTITY_NAME.CREATED_AT, entity.createdAt)
.returning()
.fetchOne()!!
.toEntity()
private fun EntityNameRecord.toEntity() = EntityName(
id = id,
name = name,
createdAt = createdAt,
updatedAt = updatedAt
)
}
@RestController
class EntityNameController(
private val service: EntityNameService
) : EntityNameApi {
override fun create(request: CreateRequest): ResponseEntity<EntityResponse> {
val (result, isNew) = service.create(request)
return if (isNew) ResponseEntity.status(201).body(result)
else ResponseEntity.ok(result)
}
override fun getById(id: UUID): ResponseEntity<EntityResponse> {
val entity = service.findById(id)
?: throw ResourceNotFoundRestException("EntityName", id)
return ResponseEntity.ok(entity.toResponse())
}
}
@Tag(name = "Entity Name")
interface EntityNameApi {
@Operation(summary = "Create entity")
@PostMapping("/api/v1/entities")
fun create(@RequestBody @Valid request: CreateRequest): ResponseEntity<EntityResponse>
@Operation(summary = "Get entity by ID")
@GetMapping("/api/v1/entities/{id}")
fun getById(@PathVariable id: UUID): ResponseEntity<EntityResponse>
}
data class CreateRequest(
@field:NotBlank
val name: String,
@field:Size(max = 255)
val description: String?
)
data class EntityResponse(
val id: UUID,
val name: String,
val description: String?,
val createdAt: Instant
)
Typed hierarchy at service layer; @ControllerAdvice translates to RFC 7807 ProblemDetail (Spring 6+ / Boot 4 canonical shape, application/problem+json).
// Throw typed exceptions from services / controllers
throw ResourceNotFoundRestException("EntityName", id)
throw ValidationRestException("Name cannot be empty")
throw ConflictRestException("Entity already exists")
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundRestException::class)
fun handleNotFound(ex: ResourceNotFoundRestException): ResponseEntity<ProblemDetail> {
val pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.message)
pd.title = "Resource not found"
pd.setProperty("resource", ex.resource)
pd.setProperty("id", ex.id)
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(pd)
}
@ExceptionHandler(ValidationRestException::class)
fun handleValidation(ex: ValidationRestException): ResponseEntity<ProblemDetail> {
val pd = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.message)
pd.title = "Validation failed"
return ResponseEntity.badRequest().body(pd)
}
@ExceptionHandler(ConflictRestException::class)
fun handleConflict(ex: ConflictRestException): ResponseEntity<ProblemDetail> {
val pd = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.message)
pd.title = "Conflict"
return ResponseEntity.status(HttpStatus.CONFLICT).body(pd)
}
}
ProblemDetail auto-serializes to application/problem+json. Use setProperty for extension fields. Do NOT hand-roll ErrorResponse DTOs.
?.let{} for optional operationswhen for exhaustive matching.single() or .firstOrNull()Pair<Result, Boolean> for idempotent operationsdevelopment
Effective Go patterns — idiomatic code, testing, benchmarks, project layout. Always use Go 1.21+ patterns.
development
Go microservices — gRPC, REST, cloud-native patterns, service discovery, circuit breakers, observability, health checks, graceful shutdown.
development
Go concurrency mastery — goroutines, channels, context, sync primitives, patterns, performance.
testing
Android WorkManager for guaranteed background execution - use for deferred tasks, periodic syncs, file uploads, notifications, and task chains. Covers CoroutineWorker, constraints, chaining, testing, and troubleshooting. Use when implementing background work that needs reliable execution across app restarts and doze mode.