Java and Kotlin
Purpose
Write JVM code that uses the modern language surface — records, sealed types, pattern matching, coroutines — instead of the 2010-era idioms most JVM codebases are still carrying.
When to Use
- Writing or reviewing Java 17+ or Kotlin.
- Migrating a codebase off Java 8 idioms.
- Designing domain models with records and sealed interfaces.
- Introducing coroutines to replace callback or thread-pool code.
- Diagnosing JVM memory, GC, or startup problems.
Capabilities
- Java: records, sealed interfaces, pattern matching for
switch, virtual threads, Optional discipline.
- Kotlin: coroutines, structured concurrency, flows, null safety, delegation, DSL builders.
- Java/Kotlin interop, including platform-type null hazards.
- Build configuration for Gradle (Kotlin DSL) and Maven.
- JVM tuning: heap sizing, GC selection, JFR profiling.
Inputs
- Source module, build file, and target JVM version.
- Whether the code is a library (binary compatibility matters) or an application.
Outputs
- Modern, immutable-by-default domain types.
- Concurrency that is structured and cancellable.
- Build configuration pinned to a specific toolchain.
Workflow
- Model — Domain values as records (Java) or data classes (Kotlin). Variants as sealed interfaces.
- Match exhaustively — Use pattern-matching
switch / when so a new variant becomes a compile error.
- Structure concurrency — Virtual threads (Java 21+) or coroutine scopes tied to a lifecycle.
- Guard nulls at the interop line — Every value crossing from Java into Kotlin is a platform type; annotate or validate it.
- Gate — Compile with warnings as errors; run SpotBugs/detekt and the test suite.
Best Practices
- Never return
null from a public API. Return Optional, an empty collection, or a sealed "absent" variant.
- Records are for data, not for behavior with hidden state. Keep them free of mutable references.
- In Kotlin,
!! is a defect marker. If you cannot remove it, the type is lying.
- Never launch a coroutine in
GlobalScope. It has no cancellation owner.
- Prefer
runCatching only in application boundaries; libraries throw typed exceptions.
- Pin the toolchain in the build file so the JVM version is not an environment variable.
Examples
Sealed model with exhaustive matching (Java 21):
sealed interface Payment permits Card, Transfer, Wallet {}
record Card(String last4, YearMonth expiry) implements Payment {}
record Transfer(String iban) implements Payment {}
record Wallet(String provider, String token) implements Payment {}
static String describe(Payment payment) {
return switch (payment) {
case Card c -> "Card ending %s".formatted(c.last4());
case Transfer t -> "Transfer to %s".formatted(t.iban());
case Wallet w -> "%s wallet".formatted(w.provider());
};
}
Structured concurrency in Kotlin:
suspend fun loadDashboard(userId: String): Dashboard = coroutineScope {
val profile = async { userService.profile(userId) }
val orders = async { orderService.recent(userId, limit = 10) }
Dashboard(profile.await(), orders.await())
}
Notes
- Virtual threads (Java 21) remove most reasons to use a reactive framework for I/O-bound work. They do not help CPU-bound work.
coroutineScope propagates cancellation to children and rethrows the first failure; supervisorScope isolates failures. Choose deliberately.
- Kotlin's
Flow is cold. Collecting it twice runs the producer twice — a common source of duplicated API calls.
1---2name: java-kotlin3description: Use when writing modern Java (17+) or Kotlin on the JVM. Covers records, sealed interfaces, pattern matching, coroutines, null safety, and JVM performance and memory tuning.4---56# Java and Kotlin78## Purpose910Write JVM code that uses the modern language surface — records, sealed types, pattern matching, coroutines — instead of the 2010-era idioms most JVM codebases are still carrying.1112## When to Use1314- Writing or reviewing Java 17+ or Kotlin.15- Migrating a codebase off Java 8 idioms.16- Designing domain models with records and sealed interfaces.17- Introducing coroutines to replace callback or thread-pool code.18- Diagnosing JVM memory, GC, or startup problems.1920## Capabilities2122- Java: records, sealed interfaces, pattern matching for `switch`, virtual threads, `Optional` discipline.23- Kotlin: coroutines, structured concurrency, flows, null safety, delegation, DSL builders.24- Java/Kotlin interop, including platform-type null hazards.25- Build configuration for Gradle (Kotlin DSL) and Maven.26- JVM tuning: heap sizing, GC selection, JFR profiling.2728## Inputs2930- Source module, build file, and target JVM version.31- Whether the code is a library (binary compatibility matters) or an application.3233## Outputs3435- Modern, immutable-by-default domain types.36- Concurrency that is structured and cancellable.37- Build configuration pinned to a specific toolchain.3839## Workflow40411. **Model** — Domain values as records (Java) or data classes (Kotlin). Variants as sealed interfaces.422. **Match exhaustively** — Use pattern-matching `switch` / `when` so a new variant becomes a compile error.433. **Structure concurrency** — Virtual threads (Java 21+) or coroutine scopes tied to a lifecycle.444. **Guard nulls at the interop line** — Every value crossing from Java into Kotlin is a platform type; annotate or validate it.455. **Gate** — Compile with warnings as errors; run SpotBugs/detekt and the test suite.4647## Best Practices4849- Never return `null` from a public API. Return `Optional`, an empty collection, or a sealed "absent" variant.50- Records are for data, not for behavior with hidden state. Keep them free of mutable references.51- In Kotlin, `!!` is a defect marker. If you cannot remove it, the type is lying.52- Never launch a coroutine in `GlobalScope`. It has no cancellation owner.53- Prefer `runCatching` only in application boundaries; libraries throw typed exceptions.54- Pin the toolchain in the build file so the JVM version is not an environment variable.5556## Examples5758**Sealed model with exhaustive matching (Java 21):**5960```java61sealed interface Payment permits Card, Transfer, Wallet {}6263record Card(String last4, YearMonth expiry) implements Payment {}64record Transfer(String iban) implements Payment {}65record Wallet(String provider, String token) implements Payment {}6667static String describe(Payment payment) {68 return switch (payment) {69 case Card c -> "Card ending %s".formatted(c.last4());70 case Transfer t -> "Transfer to %s".formatted(t.iban());71 case Wallet w -> "%s wallet".formatted(w.provider());72 };73}74```7576**Structured concurrency in Kotlin:**7778```kotlin79suspend fun loadDashboard(userId: String): Dashboard = coroutineScope {80 val profile = async { userService.profile(userId) }81 val orders = async { orderService.recent(userId, limit = 10) }82 Dashboard(profile.await(), orders.await())83}84```8586## Notes8788- Virtual threads (Java 21) remove most reasons to use a reactive framework for I/O-bound work. They do not help CPU-bound work.89- `coroutineScope` propagates cancellation to children and rethrows the first failure; `supervisorScope` isolates failures. Choose deliberately.90- Kotlin's `Flow` is cold. Collecting it twice runs the producer twice — a common source of duplicated API calls.