senior-jvm
Senior JVM developer: Java, Kotlin, Spring Boot, Ktor, Android
Senior JVM developer aggregating Java 21+ (Spring Boot, JPA, JVM tuning), Kotlin (coroutines, Ktor, Exposed, multiplatform), and Android (Jetpack Compose, ViewModel, Room, Hilt).
Java 21+ Features
| Feature | Use Case | Example |
|---------|----------|---------|
| Virtual Threads (Project Loom) | I/O-bound services, high-concurrency servers | Executors.newVirtualThreadPerTaskExecutor() |
| Pattern Matching switch | Exhaustive type dispatch | switch (obj) { case String s -> ... } |
| Record Patterns | Destructuring records in pattern match | case Point(int x, int y) -> ... |
| Sealed Classes | Closed type hierarchies | sealed interface Result permits Ok, Error |
| Structured Concurrency | Scoped task groups | StructuredTaskScope for fail-fast/fail-all |
| Scoped Values | Immutable per-thread context (replace ThreadLocal) | ScopedValue.where(CTX, ctx).run(...) |
Kotlin Coroutines & Flow
suspend fun <T> fetchAllOrFail(
vararg deferreds: Deferred<T>
): List<T> = coroutineScope {
deferreds.map { it.await() }
}
fun getData(): Flow<Result> = flow {
emit(source.load())
}.retry(3) { e ->
delay(1000L * (it + 1))
e is IOException
}.catch { emit(Result.Error(it)) }
@JvmName("getUsersAsync")
suspend fun getUsers(): List<User> = withContext(Dispatchers.IO) {
repository.findAll()
}
Spring Boot Patterns
@Transactional
fun execute(command: CreateOrder): OrderId {
val customer = customerPort.load(command.customerId)
?: throw CustomerNotFound(command.customerId)
require(customer.canPlaceOrder()) { "Order limit exceeded" }
return orderPort.save(Order.create(command, customer)).id
}
@Bean
fun threadPoolTaskExecutor(): TaskExecutor = SimpleAsyncTaskExecutor().apply {
setVirtualThreads(true)
}
@Mapper
interface OrderMapper {
fun toDto(order: Order): OrderDto
fun toEntity(dto: CreateOrderRequest): Order
}
Ktor Server
fun Application.module(secret: String) {
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
install(Authentication) {
bearer { validate { token -> if (token == secret) UserIdPrinciple(1) else null } }
}
install(StatusPages) {
exception<ValidationException> { _, call -> call.respond(HttpStatusCode.BadRequest) }
}
routing {
authenticate {
get("/api/orders/{id}") {
val id = call.parameters["id"]?.toIntOrNull() ?: throw ValidationException("Invalid id")
call.respond(orderService.getById(id))
}
post("/api/orders") {
val command = call.receive<CreateOrder>()
call.respond(HttpStatusCode.Created, orderService.execute(command))
}
}
}
}
JPA & Exposed
| Concern | JPA (Spring Data) | Exposed (Kotlin) |
|---------|-------------------|------------------|
| Query type | JPQL / Criteria | Kotlin DSL |
| Lazy loading | Proxy-based | Explicit with() |
| Schema gen | Auto-ddl / Flyway | Migration DSL |
| Best for | Existing Java services | Greenfield Kotlin |
object Orders : IntIdTable("orders") {
val customerId = reference("customer_id", Customers)
val status = enumerationByName("status", 32, OrderStatus::class)
val total = decimal("total", 12, 2)
}
class OrderDao(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<OrderDao>(Orders)
var customerId by Orders.customerId
var status by Orders.status
var total by Orders.total
}
@Entity @Table(name = "orders")
data class Order(
@Id @GeneratedValue val id: Long? = null,
@ManyToOne(fetch = FetchType.LAZY) val customer: Customer,
@Enumerated(EnumType.STRING) val status: OrderStatus,
val total: BigDecimal
)
Android Compose + Architecture
@HiltViewModel
class OrderViewModel @Inject constructor(
private val orderRepo: OrderRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _uiState = MutableStateFlow(OrderUiState())
val uiState: StateFlow<OrderUiState> = _uiState.asStateFlow()
fun load(orderId: Long) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
orderRepo.getOrder(orderId).collect { order ->
_uiState.update { it.copy(order = order, isLoading = false) }
}
}
}
}
@Composable
fun OrderScreen(
viewModel: OrderViewModel = hiltViewModel(),
onNavigateBack: () -> Unit
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
OrderContent(
uiState = uiState,
onRefresh = { viewModel.load(uiState.orderId) },
onBack = onNavigateBack
)
}
@Entity(tableName = "orders")
data class OrderEntity(
@PrimaryKey val id: Long,
@ColumnInfo(name = "customer_name") val customerName: String,
val status: String,
val total: Double
)
@Dao
interface OrderDao {
@Query("SELECT * FROM orders WHERE id = :id")
fun observeOrder(id: Long): Flow<OrderEntity?>
@Upsert
suspend fun upsert(orders: List<OrderEntity>)
}
Kotlin Multiplatform
| Layer | Shared Module | Platform | |-------|---------------|----------| | Networking | Ktor Client | OkHttp (Android), NSURLSession (iOS) | | Serialization | kotlinx.serialization | Same on all targets | | Storage | SQLDelight / Multiplatform Settings | Platform-specific driver | | DI | Koin / kotlin-inject | Same on all targets | | UI | Compose Multiplatform | Native canvas | | Coroutines | kotlinx.coroutines | Dispatchers.Default everywhere |
class ApiClient(private val httpClient: HttpClient) {
suspend fun getOrders(): List<Order> = httpClient
.get("https://api.example.com/orders")
.body()
}
expect fun buildHttpEngine(): HttpClientEngine
actual fun buildHttpEngine(): HttpClientEngine = OkHttp.create {
config { retryOnConnectionFailure(true); connectTimeout(30, TimeUnit.SECONDS) }
}
actual fun buildHttpEngine(): HttpClientEngine = Darwin.create {
configureRequest { setAllowsCellularAccess(true) }
}
Build Tooling
[versions]
kotlin = "2.1.0"
springBoot = "3.4.0"
agp = "8.7.0"
[libraries]
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter-web" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
Testing
| Tool | Scope | Key Feature |
|------|-------|-------------|
| JUnit 5 + ParameterizedTests | Unit / integration | @CsvSource, @MethodSource |
| kotlin.test | Kotlin-first | assertIs, assertContentEquals |
| MockK | Kotlin mocks | coEvery, coVerify for coroutines |
| Spring Boot Test | Slice tests | @WebMvcTest, @DataJpaTest |
| Turbine | Flow testing | flow.test { awaitItem(); awaitComplete() } |
| Compose UI Test | Android UI | ComposeTestRule, semantics |
class OrderServiceTest {
private val repo = mockk<OrderRepository>()
@Test
fun `create order validates customer`() = runTest {
coEvery { repo.save(any()) } throws CustomerNotFound(1)
assertFailsWith<CustomerNotFound> { service.execute(CreateOrder(customerId = 1)) }
coVerify(exactly = 0) { repo.save(any()) }
}
@Test
fun `flow emits loading then success`() = runTest {
val flow = service.getOrder(1)
flow.test {
assertEquals(Loading, awaitItem())
assertEquals(Order(...), awaitItem())
awaitComplete()
}
}
}
JVM Tuning
-XX:+UseZGC -XX:MaxHeapSize=2g -XX:InitialHeapSize=2g -XX:+ZGenerational
-XX:+ParallelRefProcEnabled -XX:+AlwaysPreTouch -XX:-OmitStackTraceInFastThrow
-Djava.util.concurrent.ForkJoinPool.common.parallelism=4 -XX:ActiveProcessorCount=4
-Dspring.threads.virtual.enabled=true
-Xlog:gc*:file=gclog/gc-%t.log:time,tid,tags:filecount=5,filesize=10M
Anti-Patterns
| Anti-Pattern | Fix |
|---|---|
| ThreadLocal in virtual threads | Replace with ScopedValue or pass context explicitly |
| @Autowired field injection | Constructor injection (mandatory in Kotlin) |
| GlobalScope.launch | Inject CoroutineScope or use viewModelScope |
| RxJava + coroutines in same project | Pick one: coroutines/Flow are standard |
| var in data classes | Prefer val with copy() for immutability |
| Dagger components in Android | Migrate to Hilt for standard DI |
| JPA FetchType.EAGER | Always LAZY; use @EntityGraph or fetch join |
| Blocking calls on Dispatchers.Main | Wrap in withContext(Dispatchers.IO) |