Kotlin Developer Skill
Iron Law
No new behaviour without a test that fails first, then passes.
Before Taking Any Action
- Announce what you intend to do and why
- Explain the approach, data model decisions, Spring configuration, security implications, migration steps, trade-offs
- Ask for confirmation before writing or editing any file, running any command, or executing any database operation
- Report what was created or changed, and flag follow-up items (new env vars, Flyway migrations to run, Spring Security config to update)
Task Approach
Use this table to determine what to produce for each task type:
| User asks for |
What to produce |
| New feature / endpoint |
Clarify idempotency, consistency, and compliance requirements; propose data model and API contract first; search codebase for reuse candidates; implement thin controller → service → adapter/repository with validation at the controller boundary and domain exceptions mapped at the adapter boundary |
| Bug fix |
Reproduce with a failing test first; identify whether the fault is in controller, service, adapter, or data layer; fix at the root cause and confirm the test passes |
| Data model / schema |
Normalised table definitions, surrogate key choice, index plan for every query path, Flyway migration (forward-only, backward-compatible, zero-downtime), EXPLAIN ANALYZE for non-trivial queries |
| Code review |
Per-layer feedback: constructor injection, resilience wrapping on external calls, @Transactional scope, BigDecimal for currency, idempotency of financial operations, PII/card data absent from logs, Flyway migration safety, index coverage, metrics on new external calls |
| API design |
RESTful resource structure, HTTP status code table, Problem Details error shape (RFC 9457), OpenAPI (Springdoc) spec, Bean Validation placement |
| Testing |
Unit test with @WebMvcTest + MockK/Mockito-Kotlin for controllers and services; Testcontainers integration test for repositories; WireMock for outbound HTTP; property-based tests for financial edge cases |
| Observability / metrics |
Micrometer counter/timer with {org}.{domain}.{action} naming, result tag (success/failure), OkHttp metrics listener registration, KotlinLogging lambda form with correlation and entity IDs |
| Performance optimisation |
Identify bottleneck with EXPLAIN ANALYZE or profiling; tune HikariCP pool size; introduce coroutine parallelism (async/awaitAll) for independent I/O; cache stable reads with @Cacheable |
| Security configuration |
Spring Security JWT/OAuth2 resource server config in dedicated SecurityConfig, @PreAuthorize placement, secret storage guidance, fintech compliance checklist |
Architecture Decision Review
Before designing or implementing anything non-trivial, identify which architectural decisions are in play. For each that applies, follow this pattern:
- Name the decision, what needs to be chosen and why it matters here
- Present at least 3 options, with pros, cons, and the conditions under which each is the right choice
- Recommend one, state which you recommend for this specific context and why
- Ask the user to confirm or choose, do not proceed until the key decisions are confirmed
Common decisions to look for (apply only those relevant to the task):
| Decision area |
Examples of options to present |
| Caching strategy |
(1) No cache, simplest, always fresh; (2) @Cacheable with TTL, reduces load, tolerates staleness; (3) @CacheEvict on write (event-driven invalidation), fresh on write, more complex; (4) Write-through, always consistent, write overhead. Recommend based on read/write ratio and freshness requirements. |
| Consistency model |
(1) Strong consistency, SERIALIZABLE transactions, required for financial writes; (2) Eventual consistency, async propagation, suits feeds/analytics; (3) Read-your-writes, middle ground for user-facing writes. Recommend based on data criticality. |
| Communication pattern |
(1) Synchronous REST/gRPC, simple, immediate response, tight coupling; (2) Async messaging (Kafka/SQS), decoupled, durable, adds operational complexity; (3) Hybrid, sync for commands needing a result, async for side-effects. Recommend based on latency requirements and coupling tolerance. |
| External API integration |
(1) Call on every request, simplest, always fresh, may be costly or rate-limited; (2) Cache with TTL, reduces calls, introduces staleness; (3) Background sync + local store, most resilient, adds sync complexity. Use resilience4j circuit breaker + backoff regardless of choice. |
| Data ownership |
(1) Own the data locally, fast reads, sync burden; (2) Fetch from source service at runtime, always fresh, adds latency and coupling; (3) CQRS read model, optimised reads, eventual consistency. Recommend based on read frequency and staleness tolerance. |
| Scalability approach |
(1) Vertical scaling, simple, has a ceiling; (2) Horizontal scaling with stateless design, flexible, requires externalised state; (3) Queue-based load levelling, smooths bursts, adds async complexity. Recommend based on bottleneck type (read/write/compute). |
Not every decision applies to every task. Identify the ones that do, present the options, make a recommendation, and confirm with the user before writing code.
Fintech Rules (Non-Negotiable)
- Currency: always
BigDecimal, never Double or Float
- Every financial operation must be idempotent, enforce at the API and database layer
- Audit trails are immutable, never update, only append
- Never log, store, or transmit payment card data or PII in plaintext
- Transaction isolation: use
SERIALIZABLE for financial writes; understand the implications before defaulting to READ_COMMITTED
@Transactional scope must not span external API calls, hold DB locks for DB work only
Layer Conventions
Controller
@RestController + @RequestMapping
- Primary constructor injection only, never
@Autowired
- Default
@RequestParam values inline at the parameter
@ResponseStatus(HttpStatus.NO_CONTENT) on delete endpoints
- No business logic, delegate entirely to service layer
Service
Adapter / Client
@Repository or @Component depending on role
- HTTP clients: register metrics event listener (e.g. OkHttp
OkHttpMetricsEventListener)
- gRPC: use coroutine stubs, bridge to sync with
runBlocking
- Map HTTP errors to domain exceptions at the adapter boundary, never in service or controller:
- 404 →
NotFoundException
- 429 →
TooManyRequestsException
- 403 →
ForbiddenException
Repository
- Aggregates multiple clients; wraps all calls with the resilience executor
- Use Spring Data JPA for standard CRUD; drop to JDBC or native SQL for complex queries
Domain Models
data class for all DTOs and domain objects
- Nullable fields with
? for optional attributes
- Collection fields default to
emptyList()
- No validation annotations on DTOs, validate at the controller boundary
Exceptions
Extend a base HttpException with the appropriate HttpStatus:
class NotFoundException(override val message: String) :
HttpException(status = HttpStatus.NOT_FOUND, message = message)
- All domain exceptions live in one package (e.g.
data/model/exception/)
- Use sealed result types for expected failure paths (insufficient funds, duplicate request); reserve exceptions for truly unexpected conditions
Mappers
Configuration
@ConfigurationProperties(prefix = "...") on a data class with constructor defaults
- Spring Security: configure JWT / OAuth2 resource server in a dedicated
SecurityConfig; use @PreAuthorize for method-level access control
Code Reuse & Simplicity
- Search the codebase for existing services, utilities, and Spring beans before writing new code
- Prefer extension functions and utility objects over inheritance hierarchies
- Prefer Spring's built-in abstractions (exception handlers, converters, validators) over custom frameworks
- Use sealed class hierarchies for domain result types, avoid raw exceptions for expected outcomes
- Keep controllers thin: delegate to services; keep services focused on one concern
Required Kotlin Idioms
| Situation |
Use |
| Null guard + use |
x?.let { use(it) } |
| Null fallback |
x ?: default |
| Transform + filter |
mapNotNull, filter, map |
| Index by key |
associateBy { it.id } |
| Group |
groupBy { it.type } |
| Side effect on value |
also { log(it) } |
| Enum with string ID |
enum class X(val id: String) |
| Domain result type |
sealed class Result<out T>, not nullable returns |
PostgreSQL
- Schema design: normalise balanced against query performance; every unbounded query path needs a covering index
- Indexing: B-tree, partial, composite, covering indexes; use
EXPLAIN ANALYZE before shipping any new query
- ACID transactions; advisory locks for distributed coordination
- Row-level security for multi-tenant data isolation
- Flyway: version-controlled, forward-only, backward-compatible, zero-downtime migrations
- Connection pooling: tune HikariCP (
maximumPoolSize, connectionTimeout) and monitor pool metrics
API Design
- RESTful resource design: idempotent operations, correct HTTP status codes, explicit versioning strategy
- Error responses: Problem Details (RFC 9457),
type, title, status, detail
- OpenAPI (Springdoc): document all endpoints; treat as a first-class deliverable
- Input validation: Bean Validation at the controller boundary; fail fast before any business logic runs
Testing
Unit Tests (controllers, services, pure logic)
@ExtendWith(SpringExtension::class)
@WebMvcTest(controllers = [MyController::class])
class MyControllerTest {
@Autowired private lateinit var mockMvc: MockMvc
@MockitoBean private lateinit var myService: MyService
@Test
fun `action should return expected result when condition`() { ... }
}
- Test names: backtick strings,
action should result when condition
- Mockito-Kotlin DSL:
whenever(...).thenReturn(...), verify(service).method()
- For pure Kotlin logic without Spring context, prefer MockK (Kotlin-native)
- Async assertions:
verify(service, timeout(1000)).method()
- Coroutine tests:
runTest { ... }
Integration Tests (adapters, repositories, external APIs)
- Use Testcontainers for real PostgreSQL and Redis, never mock the database
- Use an abstract base class that starts WireMock and resets it in
@AfterEach
- Verify outbound requests:
wireMockServer.verify(putRequestedFor(urlEqualTo(...)))
- Use
@Transactional rollback on DB integration tests to keep state clean
- Consider property-based testing for financial calculations where edge cases are numerous
Test Fixtures
- Add builders/factories to a shared fixture file rather than building complex objects inline in each test
Coverage Requirement
Every public method: happy path + at least one error/edge case.
Observability
Metrics
Counter.builder("{org}.{domain}.{action}")
.description("...")
.tags(Tags.of("result", result))
.register(meterRegistry)
- Naming:
{org}.{domain}.{action}
- Tag with at least
result (success/failure)
- Register a metrics event listener on every HTTP client (e.g.
OkHttpMetricsEventListener)
Logging
private val logger = KotlinLogging.logger {}
logger.info { "Message with $variable" }
logger.warn(ex) { "Failed to do X for id=$id" }
- Always use lambda form
{ }, avoids string construction when log level is disabled
- Include correlation ID and transaction/entity IDs in every log message
- Never log PII or payment card data
Checklist Before Submitting
Output Protocol
End every response with a confidence signal on its own line:
CONFIDENCE: [High|Medium|Low], [one-line reason]
- High, output is complete, correct, and based on sufficient context
- Medium, output is reasonable but contains an assumption or a gap; state the assumption inline
- Low, insufficient context to produce a reliable result; state what is missing
If the task is outside this skill's scope or you lack the information needed to proceed, return this instead of a confidence signal:
BLOCKED: [reason], [what information would unblock this]
Do not guess or produce low-quality output to avoid returning BLOCKED. A precise BLOCKED is more useful than a low-confidence guess.
1---2name: kotlin-dev3description: Use when implementing any feature or bugfix in a Kotlin/Spring Boot service, covers layer conventions, idioms, testing, and observability4---56# Kotlin Developer Skill78## Iron Law910```11No new behaviour without a test that fails first, then passes.12```1314---1516## Before Taking Any Action17181. **Announce** what you intend to do and why192. **Explain the approach**, data model decisions, Spring configuration, security implications, migration steps, trade-offs203. **Ask for confirmation** before writing or editing any file, running any command, or executing any database operation214. **Report** what was created or changed, and flag follow-up items (new env vars, Flyway migrations to run, Spring Security config to update)2223---2425## Task Approach2627Use this table to determine what to produce for each task type:2829| User asks for | What to produce |30|---|---|31| New feature / endpoint | Clarify idempotency, consistency, and compliance requirements; propose data model and API contract first; search codebase for reuse candidates; implement thin controller → service → adapter/repository with validation at the controller boundary and domain exceptions mapped at the adapter boundary |32| Bug fix | Reproduce with a failing test first; identify whether the fault is in controller, service, adapter, or data layer; fix at the root cause and confirm the test passes |33| Data model / schema | Normalised table definitions, surrogate key choice, index plan for every query path, Flyway migration (forward-only, backward-compatible, zero-downtime), `EXPLAIN ANALYZE` for non-trivial queries |34| Code review | Per-layer feedback: constructor injection, resilience wrapping on external calls, `@Transactional` scope, `BigDecimal` for currency, idempotency of financial operations, PII/card data absent from logs, Flyway migration safety, index coverage, metrics on new external calls |35| API design | RESTful resource structure, HTTP status code table, Problem Details error shape (RFC 9457), OpenAPI (Springdoc) spec, Bean Validation placement |36| Testing | Unit test with `@WebMvcTest` + MockK/Mockito-Kotlin for controllers and services; Testcontainers integration test for repositories; WireMock for outbound HTTP; property-based tests for financial edge cases |37| Observability / metrics | Micrometer counter/timer with `{org}.{domain}.{action}` naming, `result` tag (success/failure), OkHttp metrics listener registration, `KotlinLogging` lambda form with correlation and entity IDs |38| Performance optimisation | Identify bottleneck with `EXPLAIN ANALYZE` or profiling; tune HikariCP pool size; introduce coroutine parallelism (`async`/`awaitAll`) for independent I/O; cache stable reads with `@Cacheable` |39| Security configuration | Spring Security JWT/OAuth2 resource server config in dedicated `SecurityConfig`, `@PreAuthorize` placement, secret storage guidance, fintech compliance checklist |4041---4243## Architecture Decision Review4445Before designing or implementing anything non-trivial, identify which architectural decisions are in play. For each that applies, follow this pattern:46471. **Name the decision**, what needs to be chosen and why it matters here482. **Present at least 3 options**, with pros, cons, and the conditions under which each is the right choice493. **Recommend one**, state which you recommend for this specific context and why504. **Ask the user to confirm or choose**, do not proceed until the key decisions are confirmed5152Common decisions to look for (apply only those relevant to the task):5354| Decision area | Examples of options to present |55|---|---|56| **Caching strategy** | (1) No cache, simplest, always fresh; (2) `@Cacheable` with TTL, reduces load, tolerates staleness; (3) `@CacheEvict` on write (event-driven invalidation), fresh on write, more complex; (4) Write-through, always consistent, write overhead. Recommend based on read/write ratio and freshness requirements. |57| **Consistency model** | (1) Strong consistency, `SERIALIZABLE` transactions, required for financial writes; (2) Eventual consistency, async propagation, suits feeds/analytics; (3) Read-your-writes, middle ground for user-facing writes. Recommend based on data criticality. |58| **Communication pattern** | (1) Synchronous REST/gRPC, simple, immediate response, tight coupling; (2) Async messaging (Kafka/SQS), decoupled, durable, adds operational complexity; (3) Hybrid, sync for commands needing a result, async for side-effects. Recommend based on latency requirements and coupling tolerance. |59| **External API integration** | (1) Call on every request, simplest, always fresh, may be costly or rate-limited; (2) Cache with TTL, reduces calls, introduces staleness; (3) Background sync + local store, most resilient, adds sync complexity. Use resilience4j circuit breaker + backoff regardless of choice. |60| **Data ownership** | (1) Own the data locally, fast reads, sync burden; (2) Fetch from source service at runtime, always fresh, adds latency and coupling; (3) CQRS read model, optimised reads, eventual consistency. Recommend based on read frequency and staleness tolerance. |61| **Scalability approach** | (1) Vertical scaling, simple, has a ceiling; (2) Horizontal scaling with stateless design, flexible, requires externalised state; (3) Queue-based load levelling, smooths bursts, adds async complexity. Recommend based on bottleneck type (read/write/compute). |6263Not every decision applies to every task. Identify the ones that do, present the options, make a recommendation, and confirm with the user before writing code.6465---6667## Fintech Rules (Non-Negotiable)6869- Currency: always `BigDecimal`, never `Double` or `Float`70- Every financial operation must be idempotent, enforce at the API and database layer71- Audit trails are immutable, never update, only append72- Never log, store, or transmit payment card data or PII in plaintext73- Transaction isolation: use `SERIALIZABLE` for financial writes; understand the implications before defaulting to `READ_COMMITTED`74- `@Transactional` scope must not span external API calls, hold DB locks for DB work only7576---7778## Layer Conventions7980### Controller81- `@RestController` + `@RequestMapping`82- **Primary constructor injection only**, never `@Autowired`83- Default `@RequestParam` values inline at the parameter84- `@ResponseStatus(HttpStatus.NO_CONTENT)` on delete endpoints85- No business logic, delegate entirely to service layer8687### Service88- `@Service` annotation89- `@Cacheable` / `@CacheEvict` for cached reads of stable data90- Concurrent I/O with coroutines:91 ```kotlin92 runBlocking(Dispatchers.IO) {93 items.map { async { fetch(it) } }.awaitAll()94 }95 ```96- Fire-and-forget async: `CoroutineScope(Dispatchers.IO).launch { try { ... } catch (e: Exception) { logger.error(e) { "..." } } }`97- Wrap all external calls through a resilience executor (circuit breaker / retry)9899### Adapter / Client100- `@Repository` or `@Component` depending on role101- HTTP clients: register metrics event listener (e.g. OkHttp `OkHttpMetricsEventListener`)102- gRPC: use coroutine stubs, bridge to sync with `runBlocking`103- **Map HTTP errors to domain exceptions at the adapter boundary**, never in service or controller:104 - 404 → `NotFoundException`105 - 429 → `TooManyRequestsException`106 - 403 → `ForbiddenException`107108### Repository109- Aggregates multiple clients; wraps all calls with the resilience executor110- Use Spring Data JPA for standard CRUD; drop to JDBC or native SQL for complex queries111112### Domain Models113- `data class` for all DTOs and domain objects114- Nullable fields with `?` for optional attributes115- Collection fields default to `emptyList()`116- No validation annotations on DTOs, validate at the controller boundary117118### Exceptions119Extend a base `HttpException` with the appropriate `HttpStatus`:120```kotlin121class NotFoundException(override val message: String) :122 HttpException(status = HttpStatus.NOT_FOUND, message = message)123```124- All domain exceptions live in one package (e.g. `data/model/exception/`)125- Use sealed result types for expected failure paths (insufficient funds, duplicate request); reserve exceptions for truly unexpected conditions126127### Mappers128- `object` with extension functions on receiver types, not a Spring bean:129 ```kotlin130 object DomainMapper {131 fun ContentfulDto.toDomain(): DomainModel = ...132 }133 ```134- Return `null` when required data is absent; use `mapNotNull` at call sites135136### Configuration137- `@ConfigurationProperties(prefix = "...")` on a `data class` with constructor defaults138- Spring Security: configure JWT / OAuth2 resource server in a dedicated `SecurityConfig`; use `@PreAuthorize` for method-level access control139140---141142## Code Reuse & Simplicity143144- Search the codebase for existing services, utilities, and Spring beans before writing new code145- Prefer extension functions and utility objects over inheritance hierarchies146- Prefer Spring's built-in abstractions (exception handlers, converters, validators) over custom frameworks147- Use sealed class hierarchies for domain result types, avoid raw exceptions for expected outcomes148- Keep controllers thin: delegate to services; keep services focused on one concern149150---151152## Required Kotlin Idioms153154| Situation | Use |155|---|---|156| Null guard + use | `x?.let { use(it) }` |157| Null fallback | `x ?: default` |158| Transform + filter | `mapNotNull`, `filter`, `map` |159| Index by key | `associateBy { it.id }` |160| Group | `groupBy { it.type }` |161| Side effect on value | `also { log(it) }` |162| Enum with string ID | `enum class X(val id: String)` |163| Domain result type | `sealed class Result<out T>`, not nullable returns |164165---166167## PostgreSQL168169- Schema design: normalise balanced against query performance; every unbounded query path needs a covering index170- Indexing: B-tree, partial, composite, covering indexes; use `EXPLAIN ANALYZE` before shipping any new query171- ACID transactions; advisory locks for distributed coordination172- Row-level security for multi-tenant data isolation173- Flyway: version-controlled, forward-only, backward-compatible, zero-downtime migrations174- Connection pooling: tune HikariCP (`maximumPoolSize`, `connectionTimeout`) and monitor pool metrics175176---177178## API Design179180- RESTful resource design: idempotent operations, correct HTTP status codes, explicit versioning strategy181- Error responses: Problem Details (RFC 9457), `type`, `title`, `status`, `detail`182- OpenAPI (Springdoc): document all endpoints; treat as a first-class deliverable183- Input validation: Bean Validation at the controller boundary; fail fast before any business logic runs184185---186187## Testing188189### Unit Tests (controllers, services, pure logic)190```kotlin191@ExtendWith(SpringExtension::class)192@WebMvcTest(controllers = [MyController::class])193class MyControllerTest {194 @Autowired private lateinit var mockMvc: MockMvc195 @MockitoBean private lateinit var myService: MyService196197 @Test198 fun `action should return expected result when condition`() { ... }199}200```201- Test names: backtick strings, `action should result when condition`202- Mockito-Kotlin DSL: `whenever(...).thenReturn(...)`, `verify(service).method()`203- For pure Kotlin logic without Spring context, prefer MockK (Kotlin-native)204- Async assertions: `verify(service, timeout(1000)).method()`205- Coroutine tests: `runTest { ... }`206207### Integration Tests (adapters, repositories, external APIs)208- Use Testcontainers for real PostgreSQL and Redis, never mock the database209- Use an abstract base class that starts WireMock and resets it in `@AfterEach`210- Verify outbound requests: `wireMockServer.verify(putRequestedFor(urlEqualTo(...)))`211- Use `@Transactional` rollback on DB integration tests to keep state clean212- Consider property-based testing for financial calculations where edge cases are numerous213214### Test Fixtures215- Add builders/factories to a shared fixture file rather than building complex objects inline in each test216217### Coverage Requirement218Every public method: happy path + at least one error/edge case.219220---221222## Observability223224### Metrics225```kotlin226Counter.builder("{org}.{domain}.{action}")227 .description("...")228 .tags(Tags.of("result", result))229 .register(meterRegistry)230```231- Naming: `{org}.{domain}.{action}`232- Tag with at least `result` (success/failure)233- Register a metrics event listener on every HTTP client (e.g. `OkHttpMetricsEventListener`)234235### Logging236```kotlin237private val logger = KotlinLogging.logger {}238239logger.info { "Message with $variable" }240logger.warn(ex) { "Failed to do X for id=$id" }241```242- Always use lambda form `{ }`, avoids string construction when log level is disabled243- Include correlation ID and transaction/entity IDs in every log message244- Never log PII or payment card data245246---247248## Checklist Before Submitting249250- [ ] Test written and watched fail before implementation251- [ ] No `@Autowired`, primary constructor injection only252- [ ] External calls wrapped in resilience executor253- [ ] HTTP errors mapped to domain exceptions at adapter boundary254- [ ] `@Transactional` scope does not span external API calls255- [ ] Currency values use `BigDecimal`; financial operations are idempotent256- [ ] No PII or card data in logs or error messages257- [ ] Flyway migration is backward-compatible and zero-downtime258- [ ] New unbounded queries have a covering index259- [ ] Metrics added for new external calls260- [ ] Logging uses `KotlinLogging` lambda form261- [ ] New config uses `@ConfigurationProperties` data class262- [ ] OpenAPI docs updated for new/changed endpoints263264---265266## Output Protocol267268End every response with a confidence signal on its own line:269270```271CONFIDENCE: [High|Medium|Low], [one-line reason]272```273274- **High**, output is complete, correct, and based on sufficient context275- **Medium**, output is reasonable but contains an assumption or a gap; state the assumption inline276- **Low**, insufficient context to produce a reliable result; state what is missing277278If the task is outside this skill's scope or you lack the information needed to proceed, return this instead of a confidence signal:279280```281BLOCKED: [reason], [what information would unblock this]282```283284Do not guess or produce low-quality output to avoid returning BLOCKED. A precise BLOCKED is more useful than a low-confidence guess.