---
# === IDENTITY ===
id: software/migrations/java-spring-to-kotlin/2026
canonical_question: "How do I migrate Java Spring to Kotlin Spring Boot?"
aliases:
  - "Java to Kotlin Spring migration"
  - "convert Java Spring Boot to Kotlin"
  - "migrate Spring Boot from Java to Kotlin"
  - "Java Spring Boot Kotlin conversion guide"
  - "rewrite Java Spring app in Kotlin"
  - "Spring Boot Java to Kotlin step by step"
  - "Kotlin Spring Boot migration from Java"
entity_type: software_reference
domain: software > migrations > java_spring_to_kotlin
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-05-29
confidence: 0.90
version: 2.1
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: "Spring Boot 4.0 GA + Kotlin 2.2 baseline (2025-11)"
  next_review: 2026-11-25
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Never use data class for JPA entities -- breaks equals/hashCode identity tracking and prevents Hibernate lazy-loading proxies"
  - "Requires kotlin-spring (allopen) and kotlin-jpa (noarg) compiler plugins -- without them, Spring cannot proxy classes and JPA cannot instantiate entities"
  - "Spring Boot 4.0 requires Kotlin 2.2+ -- do not use Kotlin 1.x or 2.0/2.1 with Spring Boot 4"
  - "Upgrade to Spring Boot 3.5 before migrating to 4.0 -- direct jumps from 2.x to 4.x are unsupported"
  - "Migrate from kapt to KSP before upgrading to Kotlin 2.2 -- kapt is deprecated and generates stubs that slow builds significantly"
  - "Mixed Java/Kotlin projects must compile Kotlin before Java in Maven -- otherwise Java cannot reference Kotlin classes"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User wants to migrate Java EE (not Spring Boot) to a modern framework"
    use_instead: "software/migrations/java-ee-to-spring-boot/2026"
  - condition: "User wants to migrate between Kotlin versions (e.g., Kotlin 1.x to 2.x K2 compiler)"
    use_instead: "Kotlin K2 compiler migration guide (kotlinlang.org/docs/k2-compiler-migration-guide.html)"
  - condition: "User wants to add coroutines to an existing Kotlin Spring Boot project (not migrating from Java)"
    use_instead: "Spring WebFlux with Kotlin Coroutines documentation"

# === AGENT HINTS ===
inputs_needed:
  - key: spring_boot_version
    question: "Which Spring Boot version is the project currently on?"
    type: choice
    options: ["2.x (javax.*)", "3.x (jakarta.*)", "4.x (JSpecify null-safety)"]
  - key: build_tool
    question: "Which build tool does the project use?"
    type: choice
    options: ["Gradle (Groovy DSL)", "Gradle (Kotlin DSL)", "Maven"]
  - key: jpa_usage
    question: "Does the project use JPA/Hibernate entities?"
    type: choice
    options: ["Yes -- heavy JPA usage", "Minimal JPA", "No JPA (Spring Data JDBC, R2DBC, or NoSQL)"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/migrations/java-spring-to-kotlin/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-05-29)"

# === RELATED UNITS ===
related_kos:
  depends_on:
    - id: "software/migrations/java-ee-to-spring-boot/2026"
      label: "Java EE to Spring Boot Migration"
  related_to:
    - id: "software/migrations/javascript-to-typescript/2026"
      label: "JavaScript to TypeScript Migration"
    - id: "software/migrations/python2-to-python3/2026"
      label: "Python 2 to Python 3 Migration"
  alternative_to:
    - id: "software/migrations/dotnet-framework-to-core/2026"
      label: ".NET Framework to .NET Core Migration"
  often_confused_with:
    - id: "software/migrations/java-ee-to-spring-boot/2026"
      label: "Java EE to Spring Boot Migration (different migration: framework change, not language change)"

# === SOURCES (9 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "All-open Compiler Plugin"
    author: JetBrains
    url: https://kotlinlang.org/docs/all-open-plugin.html
    type: official_docs
    published: 2025-10-01
    reliability: authoritative
  - id: src2
    title: "The Ultimate Guide to Successfully Adopting Kotlin in a Java-Dominated Environment"
    author: JetBrains Kotlin Blog
    url: https://blog.jetbrains.com/kotlin/2025/12/the-ultimate-guide-to-successfully-adopting-kotlin-in-a-java-dominated-environment/
    type: technical_blog
    published: 2025-12-15
    reliability: high
  - id: src3
    title: "The Story of a Java to Kotlin Migration"
    author: Ninja Squad
    url: https://blog.ninja-squad.com/2018/05/22/kotlin-migration/
    type: technical_blog
    published: 2018-05-22
    reliability: high
  - id: src4
    title: "How to Avoid Common Pitfalls with JPA and Kotlin"
    author: JetBrains IntelliJ IDEA Blog
    url: https://blog.jetbrains.com/idea/2026/01/how-to-avoid-common-pitfalls-with-jpa-and-kotlin/
    type: technical_blog
    published: 2026-01-15
    reliability: high
  - id: src5
    title: "Strengthening Kotlin for Backend Development: A Strategic Partnership with Spring"
    author: JetBrains Kotlin Blog
    url: https://blog.jetbrains.com/kotlin/2025/05/strategic-partnership-with-spring/
    type: industry_report
    published: 2025-05-20
    reliability: high
  - id: src6
    title: "Spring Boot 4: Leaner, Safer Apps and a New Kotlin Baseline"
    author: JetBrains IntelliJ IDEA Blog
    url: https://blog.jetbrains.com/idea/2025/11/spring-boot-4/
    type: technical_blog
    published: 2025-11-10
    reliability: high
  - id: src7
    title: "Get Started with Spring Boot and Kotlin"
    author: Kotlin Documentation
    url: https://kotlinlang.org/docs/jvm-get-started-spring-boot.html
    type: official_docs
    published: 2025-09-01
    reliability: authoritative
  - id: src8
    title: "Spring Boot 4.0 Migration Guide"
    author: Spring Projects
    url: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide
    type: official_docs
    published: 2025-11-20
    reliability: authoritative
  - id: src9
    title: "Next level Kotlin support in Spring Boot 4"
    author: Spring (spring.io blog)
    url: https://spring.io/blog/2025/12/18/next-level-kotlin-support-in-spring-boot-4/
    type: official_docs
    published: 2025-12-18
    reliability: authoritative
---

# How to Migrate Java Spring to Kotlin Spring Boot

## How do I migrate Java Spring to Kotlin Spring Boot?

## TL;DR

- **Bottom line**: Migrate incrementally -- add the `kotlin-spring` and `kotlin-jpa` Gradle/Maven plugins, convert files one at a time starting with tests and DTOs, and let Java and Kotlin coexist in the same project until migration is complete.
- **Key tool/command**: IntelliJ IDEA `Code > Convert Java File to Kotlin File` (Ctrl+Alt+Shift+K) for automated conversion, then manual cleanup for idiomatic Kotlin.
- **Watch out for**: JPA entities must NOT be `data class` -- use regular classes with `var` properties, because `data class` generates broken `equals`/`hashCode` for mutable entities and prevents lazy-loading proxies.
- **Works with**: Spring Boot 3.5.x / 4.0.x (4.0.5 current, May 2026), Kotlin 2.2+ (2.3.20 latest), Gradle (Kotlin DSL) or Maven with kotlin-maven-plugin. Spring Boot 4.0 pins Kotlin 2.2.21 in its BOM and uses JSpecify null-safety.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- **Never use `data class` for JPA entities** -- `data class` generates `equals`/`hashCode` using ALL fields, breaking identity tracking when fields change after persist. Also prevents Hibernate lazy-loading proxies because data classes are `final`. Use regular classes with ID-based `equals`/`hashCode`. [src4]
- **Both `kotlin-spring` and `kotlin-jpa` plugins are mandatory** -- Without `kotlin-spring` (allopen), all Kotlin classes are `final` and Spring cannot create CGLIB proxies, causing `BeanCreationException`. Without `kotlin-jpa` (noarg), JPA cannot instantiate entities, causing `InstantiationException`. [src1]
- **Spring Boot 4.0 requires Kotlin 2.2+** -- The managed BOM pins Kotlin 2.2.x. Do not attempt to use Kotlin 1.x or 2.0/2.1 with Spring Boot 4. JSpecify annotations may cause new compilation failures for previously nullable-ambiguous platform types. [src6, src8]
- **Upgrade path must be sequential**: Spring Boot 2.x -> 3.5 -> 4.0 -- Direct jumps from 2.x to 4.x skip the javax.* to jakarta.* namespace migration and are unsupported. [src8]
- **Migrate from kapt to KSP before Kotlin 2.2** -- kapt is deprecated and generates Java stubs that add 30-50% build overhead. KSP processes Kotlin symbols directly and is up to 2x faster. Most annotation processors (Dagger, Room, Moshi) now ship KSP artifacts. [src2]
- **In Maven, Kotlin must compile before Java** -- Mixed-language projects require kotlin-maven-plugin to run in the `compile` phase before `maven-compiler-plugin`, otherwise Java code cannot reference Kotlin classes. Gradle handles this automatically. [src7]

## Quick Reference

| Java Spring Pattern | Kotlin Equivalent | Example |
|---|---|---|
| `public class UserService` | `class UserService` (open via plugin) | `kotlin-spring` plugin auto-opens `@Service` classes |
| POJO with getters/setters | `data class UserDto(val name: String, val email: String)` | DTOs become one-line data classes |
| `@Autowired` field injection | Constructor injection (default) | `class UserService(private val repo: UserRepository)` |
| `Optional<User>` return | `User?` nullable type | `fun findById(id: Long): User?` |
| `Stream.map().filter().collect()` | `list.map { }.filter { }` | Native collection operators, no `.collect()` needed |
| `try { } catch (Exception e) { }` | `try { } catch (e: Exception) { }` | Also: `runCatching { }.getOrElse { default }` |
| `@Entity` class with no-arg ctor | `@Entity` class + `kotlin-jpa` plugin | Plugin generates synthetic no-arg constructor |
| `@RequestParam required=false` | `@RequestParam name: String?` | Nullable type replaces `required = false` |
| `new ResponseEntity<>(body, OK)` | `ResponseEntity.ok(body)` | Or use `@ResponseBody` with direct return |
| `@Value("${prop}")` field | `@Value("\${prop}")` (escaped) | Kotlin string interpolation requires `\$` escaping |
| `static` methods/constants | `companion object` | `companion object { const val MAX_RETRIES = 3 }` |
| `Lombok @Data` | `data class` | No Lombok needed -- Kotlin has it built in |
| `interface FooRepository extends JpaRepository<Foo, Long>` | `interface FooRepository : JpaRepository<Foo, Long>` | Colon replaces `extends`/`implements` |
| `Collections.unmodifiableList(list)` | `list.toList()` or declare as `List<T>` | Kotlin `List` is already read-only by default |
| Bean validation `@NotNull` | Non-nullable type `val name: String` | Kotlin null safety replaces many annotations |
| `org.springframework.lang.Nullable` | `org.jspecify.annotations.Nullable` | Spring Boot 4.0 migrated to JSpecify annotations [src8] |

## Decision Tree

```
START
+-- Is the project using Gradle?
|   +-- YES -> Add kotlin("plugin.spring") + kotlin("plugin.jpa") to plugins block
|   +-- NO (Maven) -> Add kotlin-maven-plugin with spring + jpa compiler plugins
+-- Does the project use Lombok?
|   +-- YES -> Remove Lombok first: convert @Data to records or plain classes, then to Kotlin data classes
|   +-- NO v
+-- Does the project have good test coverage?
|   +-- YES -> Convert tests to Kotlin first (safe, validates interop)
|   +-- NO -> Write Kotlin tests for existing Java code first, then convert production code
+-- Does the project use JPA/Hibernate entities?
|   +-- YES -> Do NOT use data class for entities. Use regular class + var properties + kotlin-jpa plugin
|   +-- NO v
+-- Is the project Spring Boot 3.x or 4.x?
|   +-- 4.x -> Use Kotlin 2.2+ with JSpecify null-safety; replace org.springframework.lang.Nullable with org.jspecify.annotations.Nullable
|   +-- 3.x -> Use Kotlin 2.0+ with jakarta.* imports
|   +-- 2.x -> Upgrade to Spring Boot 3.5 first, then convert to Kotlin
+-- Is the project using kapt?
|   +-- YES -> Migrate to KSP before upgrading to Kotlin 2.2 (kapt is deprecated)
|   +-- NO v
+-- DEFAULT -> Convert file-by-file: DTOs -> Services -> Controllers -> Config -> Entities (last)
```

## Decision Logic

### If the project is on Spring Boot 2.x
--> Do NOT migrate language yet. Upgrade Spring Boot 2.x to 3.5.11 first (javax.* to jakarta.* namespace migration), then convert to Kotlin. Direct 2.x to 4.x jumps are unsupported. [src8]

### If the project is on Spring Boot 3.4 or earlier 3.x
--> Spring Boot 3.4 is EOL (OSS support ended 2025-12-31). Move to 3.5.11 (supported until 2026-06-30) or 4.0.5 before investing in a Kotlin migration. [src6, src8]

### If you are targeting Spring Boot 4.0/4.1
--> Use Kotlin 2.2+ (the 4.0 BOM pins 2.2.21; 2.3.20 is the latest stable). Expect JSpecify-driven null-safety refinements -- Spring, Reactor, and Micrometer APIs now expose Kotlin null-safe types instead of platform types, so audit call sites for new `?` requirements. [src8, src9]

### If you are converting JPA/Hibernate entities
--> Never use `data class`. Use a regular `class` with `var` properties, the `kotlin-jpa` (noarg) plugin, and ID-based `equals`/`hashCode` with `hashCode()` derived from `javaClass`. [src4]

### If the project still uses kapt for annotation processing
--> Migrate to KSP (latest 2.3.7) before going to Kotlin 2.2+. kapt is in maintenance mode and is not enabled by default from Kotlin 2.0; KSP and kapt can run side-by-side so migrate module-by-module. [src2]

### If coroutines are the primary motivation for adopting Kotlin
--> Use Spring WebFlux, not Spring MVC -- only WebFlux controllers support `suspend` functions and `Flow`. On Spring Boot 4.0 enable automatic context propagation with `spring.reactor.context-propagation=auto`. [src5, src9]

### If the codebase is large with heavy Lombok use
--> Remove Lombok first (convert `@Data` to Java records or plain classes), then convert file-by-file: DTOs -> Services -> Controllers -> Config -> Entities, converting tests first to validate Java/Kotlin interop. [src2, src3]

## Step-by-Step Guide

### 1. Configure build system for Kotlin

Add the Kotlin plugin, compiler plugins for Spring and JPA, and required dependencies to your build file. The `kotlin-spring` plugin auto-opens classes annotated with `@Component`, `@Service`, `@Controller`, `@Configuration`, `@Repository`, and `@Transactional`. The `kotlin-jpa` plugin generates synthetic no-argument constructors for `@Entity`, `@MappedSuperclass`, and `@Embeddable` classes. [src1, src7]

```kotlin
// build.gradle.kts
plugins {
    id("org.springframework.boot") version "3.4.2"
    id("io.spring.dependency-management") version "1.1.7"
    kotlin("jvm") version "2.1.10"
    kotlin("plugin.spring") version "2.1.10"  // Auto-opens Spring-annotated classes
    kotlin("plugin.jpa") version "2.1.10"     // Generates no-arg constructors for JPA
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")  // JSON serialization
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

kotlin {
    jvmToolchain(17)
    compilerOptions {
        freeCompilerArgs.addAll("-Xjsr305=strict")  // Treat Spring null annotations as strict
    }
}
```

**Verify**: `./gradlew compileKotlin` succeeds with no errors. Existing Java files still compile alongside Kotlin sources.

### 2. Convert the main application class

Replace the Java application entry point with a Kotlin top-level function. The `main` function must be a package-level function, not inside a class. [src7]

```kotlin
// src/main/kotlin/com/example/Application.kt
package com.example

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)  // Kotlin-idiomatic Spring Boot startup
}
```

**Verify**: `./gradlew bootRun` starts the application. All existing Java beans are still discovered and initialized.

### 3. Convert DTOs and value objects first

Data Transfer Objects are the safest starting point -- they have no framework dependencies and benefit most from Kotlin's `data class` syntax. This typically eliminates 60-80% of boilerplate. [src3]

```java
// BEFORE: Java DTO (30 lines with Lombok, 80+ lines without)
public class UserDto {
    private final String name;
    private final String email;
    private final int age;

    public UserDto(String name, String email, int age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }

    public String getName() { return name; }
    public String getEmail() { return email; }
    public int getAge() { return age; }

    @Override
    public boolean equals(Object o) { /* ... */ }
    @Override
    public int hashCode() { /* ... */ }
    @Override
    public String toString() { /* ... */ }
}
```

```kotlin
// AFTER: Kotlin data class (1 line)
data class UserDto(val name: String, val email: String, val age: Int)
```

**Verify**: All endpoints returning DTOs serialize to identical JSON. Run existing integration tests.

### 4. Convert service classes

Replace `@Service` Java classes with Kotlin. Use constructor injection (Kotlin primary constructor), replace `Optional` with nullable types, and use expression body functions for simple methods. [src2, src3]

```java
// BEFORE: Java Service
@Service
@Transactional
public class OrderService {

    private final OrderRepository orderRepo;
    private final NotificationService notificationService;

    @Autowired
    public OrderService(OrderRepository orderRepo,
                        NotificationService notificationService) {
        this.orderRepo = orderRepo;
        this.notificationService = notificationService;
    }

    public Optional<Order> findById(Long id) {
        return orderRepo.findById(id);
    }

    public Order createOrder(OrderRequest request) {
        Order order = new Order();
        order.setProduct(request.getProduct());
        order.setQuantity(request.getQuantity());
        order.setStatus(OrderStatus.PENDING);
        Order saved = orderRepo.save(order);
        notificationService.sendConfirmation(saved);
        return saved;
    }
}
```

```kotlin
// AFTER: Kotlin Service
@Service
@Transactional
class OrderService(
    private val orderRepo: OrderRepository,
    private val notificationService: NotificationService
) {
    fun findById(id: Long): Order? = orderRepo.findById(id).orElse(null)

    fun createOrder(request: OrderRequest): Order {
        val order = Order(
            product = request.product,
            quantity = request.quantity,
            status = OrderStatus.PENDING
        )
        return orderRepo.save(order).also {
            notificationService.sendConfirmation(it)
        }
    }
}
```

**Verify**: Run `./gradlew test` -- all service-level tests pass with identical behavior.

### 5. Convert controllers

Replace `@RestController` Java classes. Use Kotlin's expression-body functions, nullable parameters for optional query params, and destructuring where useful. [src7]

```kotlin
// Kotlin REST Controller
@RestController
@RequestMapping("/api/orders")
class OrderController(private val orderService: OrderService) {

    @GetMapping("/{id}")
    fun getOrder(@PathVariable id: Long): ResponseEntity<Order> =
        orderService.findById(id)
            ?.let { ResponseEntity.ok(it) }
            ?: ResponseEntity.notFound().build()

    @GetMapping
    fun listOrders(
        @RequestParam status: OrderStatus?,      // nullable = optional
        @RequestParam(defaultValue = "20") limit: Int
    ): List<Order> = orderService.findByStatus(status, limit)

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun createOrder(@Valid @RequestBody request: OrderRequest): Order =
        orderService.createOrder(request)
}
```

**Verify**: `curl http://localhost:8080/api/orders/1` returns the same JSON as before migration.

### 6. Convert JPA entities (last)

Entities require the most care. Do NOT use `data class` for JPA entities. Use regular classes with `var` properties and rely on the `kotlin-jpa` plugin for no-arg constructors. [src4]

```java
// BEFORE: Java Entity
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String product;

    private int quantity;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    @ManyToOne(fetch = FetchType.LAZY)
    private Customer customer;

    // getters, setters, equals, hashCode...
}
```

```kotlin
// AFTER: Kotlin Entity (NOT a data class)
@Entity
@Table(name = "orders")
class Order(
    @Column(nullable = false)
    var product: String,

    var quantity: Int,

    @Enumerated(EnumType.STRING)
    var status: OrderStatus = OrderStatus.PENDING,

    @ManyToOne(fetch = FetchType.LAZY)
    var customer: Customer? = null,

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null  // nullable until persisted
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Order) return false
        return id != null && id == other.id  // ID-based equality
    }

    override fun hashCode(): Int = javaClass.hashCode()  // Consistent hash
}
```

**Verify**: `./gradlew test` passes. Verify lazy loading works: `customer` field is not eagerly fetched. Check Hibernate SQL logs.

### 7. Convert test classes

Convert JUnit tests to idiomatic Kotlin. Use backtick-quoted function names for readable test descriptions and leverage `mockk` or `mockito-kotlin` for mocking. [src3]

```kotlin
// Kotlin test with readable names
@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerTest(@Autowired val mockMvc: MockMvc) {

    @Test
    fun `should return order by ID`() {
        mockMvc.get("/api/orders/1")
            .andExpect {
                status { isOk() }
                jsonPath("$.product") { value("Widget") }
            }
    }

    @Test
    fun `should return 404 for unknown order`() {
        mockMvc.get("/api/orders/99999")
            .andExpect { status { isNotFound() } }
    }
}
```

**Verify**: `./gradlew test` -- all tests pass. Test count should remain the same.

## Code Examples

### Gradle Kotlin DSL: Complete build configuration

> Full script: [gradle-kotlin-dsl-complete-build-configuration.txt](scripts/gradle-kotlin-dsl-complete-build-configuration.txt) (45 lines)

```kotlin
// Input:  A Java Spring Boot project switching to Kotlin
// Output: Complete build.gradle.kts with all required plugins and dependencies
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
    id("org.springframework.boot") version "3.4.2"
# ... (see full script)
```

### Maven: Kotlin configuration for Spring Boot

> Full script: [maven-kotlin-configuration-for-spring-boot.txt](scripts/maven-kotlin-configuration-for-spring-boot.txt) (77 lines)

```xml
<!-- Input:  A Maven-based Java Spring Boot project adding Kotlin support
     Output: pom.xml additions for Kotlin compilation with spring/jpa plugins -->
<properties>
    <kotlin.version>2.1.10</kotlin.version>
    <java.version>17</java.version>
# ... (see full script)
```

### Kotlin: Spring Data Repository with custom queries

> Full script: [kotlin-spring-data-repository-with-custom-queries.txt](scripts/kotlin-spring-data-repository-with-custom-queries.txt) (25 lines)

```kotlin
// Input:  A repository needing both derived queries and custom JPQL
// Output: Idiomatic Kotlin Spring Data repository
interface OrderRepository : JpaRepository<Order, Long> {
    // Derived query -- Spring generates SQL from method name
    fun findByStatus(status: OrderStatus): List<Order>
# ... (see full script)
```

## Anti-Patterns

### Wrong: Using data class for JPA entities

```kotlin
// BAD -- data class generates equals/hashCode using ALL fields,
// breaking identity tracking when fields change after persist.
// Also prevents Hibernate lazy-loading proxies (data classes are final).
@Entity
data class Order(
    @Id @GeneratedValue
    val id: Long = 0,
    val product: String,
    val quantity: Int,
    @ManyToOne(fetch = FetchType.LAZY)
    val customer: Customer? = null  // Lazy proxy creation fails on final class
)
```

### Correct: Use regular class with manual equals/hashCode

```kotlin
// GOOD -- regular class allows Hibernate proxying for lazy loading.
// ID-based equals/hashCode is stable across entity lifecycle.
@Entity
class Order(
    var product: String,
    var quantity: Int,
    @ManyToOne(fetch = FetchType.LAZY)
    var customer: Customer? = null,
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Order) return false
        return id != null && id == other.id
    }
    override fun hashCode(): Int = javaClass.hashCode()
}
```

### Wrong: Field injection with lateinit var

```kotlin
// BAD -- field injection hides dependencies, makes testing harder,
// and lateinit throws UninitializedPropertyAccessException if Spring wiring fails silently.
@Service
class OrderService {
    @Autowired
    lateinit var orderRepo: OrderRepository

    @Autowired
    lateinit var notificationService: NotificationService
}
```

### Correct: Constructor injection via primary constructor

```kotlin
// GOOD -- dependencies are explicit, immutable, and fail-fast at startup.
// No @Autowired needed when there is a single constructor.
@Service
class OrderService(
    private val orderRepo: OrderRepository,
    private val notificationService: NotificationService
)
```

### Wrong: Not escaping $ in @Value annotations

```kotlin
// BAD -- Kotlin interprets ${...} as string template interpolation,
// causing a compile error or empty value injection.
@Value("${app.max-retries}")  // Kotlin tries to interpolate this!
lateinit var maxRetries: String
```

### Correct: Escape the dollar sign in @Value

```kotlin
// GOOD -- backslash escapes the dollar sign for Spring property resolution.
@Value("\${app.max-retries}")
lateinit var maxRetries: String

// BETTER -- use @ConfigurationProperties for type-safe config
@ConfigurationProperties(prefix = "app")
data class AppConfig(val maxRetries: Int = 3)
```

### Wrong: Converting Java streams literally

```kotlin
// BAD -- using Java Stream API in Kotlin instead of native collection operators.
// Verbose, non-idiomatic, and slower (stream creation overhead).
val names = users.stream()
    .filter { it.isActive }
    .map { it.name }
    .collect(Collectors.toList())
```

### Correct: Use Kotlin standard library collection operators

```kotlin
// GOOD -- Kotlin collections are more concise and perform better for small-medium lists.
val names = users.filter { it.isActive }.map { it.name }

// For large collections (100k+), use sequences for lazy evaluation
val names = users.asSequence()
    .filter { it.isActive }
    .map { it.name }
    .toList()
```

### Wrong: Using companion object for static utility methods

```kotlin
// BAD -- companion object methods are not truly static; they create
// a synthetic class and require @JvmStatic for Java interop.
class DateUtils {
    companion object {
        fun formatDate(date: LocalDate): String = date.format(DateTimeFormatter.ISO_DATE)
    }
}
```

### Correct: Use top-level functions or @JvmStatic

```kotlin
// GOOD -- top-level functions compile to real static methods.
// Better for utility functions that don't need class context.
fun formatDate(date: LocalDate): String = date.format(DateTimeFormatter.ISO_DATE)

// If Java interop is required, use @JvmStatic in companion object
class DateUtils {
    companion object {
        @JvmStatic
        fun formatDate(date: LocalDate): String = date.format(DateTimeFormatter.ISO_DATE)
    }
}
```

### Wrong: Using kapt with Kotlin 2.2+

```kotlin
// BAD -- kapt generates Java stubs, adding 30-50% build overhead.
// kapt is deprecated as of 2025 and incompatible with some K2 features.
plugins {
    kotlin("kapt") version "2.2.0"  // Deprecated!
}
dependencies {
    kapt("com.google.dagger:dagger-compiler:2.51")
}
```

### Correct: Use KSP for annotation processing

```kotlin
// GOOD -- KSP processes Kotlin symbols directly, up to 2x faster.
// Most annotation processors now ship KSP-compatible artifacts.
plugins {
    id("com.google.devtools.ksp") version "2.1.10-1.0.29"
}
dependencies {
    ksp("com.google.dagger:dagger-compiler:2.51")
}
```

## Common Pitfalls

- **Missing `kotlin-spring` plugin**: All Kotlin classes are `final` by default. Without the `kotlin-spring` (allopen) plugin, Spring cannot create CGLIB proxies for `@Service`, `@Configuration`, etc., causing `BeanCreationException`. Fix: Add `kotlin("plugin.spring")` to Gradle plugins or `<plugin>spring</plugin>` to Maven kotlin-maven-plugin. [src1]
- **Missing `kotlin-jpa` plugin**: JPA requires a no-argument constructor to instantiate entities. Kotlin classes with constructor parameters fail with `InstantiationException`. Fix: Add `kotlin("plugin.jpa")` which generates synthetic no-arg constructors for `@Entity`, `@MappedSuperclass`, `@Embeddable`. [src1, src4]
- **Missing `jackson-module-kotlin`**: Without this module, Jackson cannot deserialize into Kotlin data classes (no default no-arg constructor). Fix: Add `com.fasterxml.jackson.module:jackson-module-kotlin` dependency and ensure `KotlinModule` is registered (Spring Boot auto-registers it). [src7]
- **Using `val` for entity properties**: Hibernate may need to modify entity fields after loading (merge, lazy init). Using `val` (immutable) prevents this. Fix: Use `var` for all `@Entity` properties that Hibernate manages. Reserve `val` for DTOs and value objects. [src4]
- **Nullable ID causes issues with collections**: JPA entity ID is `Long? = null` before persist, which changes hash after `persist()`. Adding an entity to a `Set` before and after persist results in duplicates. Fix: Use consistent `hashCode()` based on `javaClass.hashCode()`, not on `id`. [src3, src4]
- **Kotlin compilation order in mixed projects**: Kotlin must compile before Java in mixed-language projects, otherwise Java code cannot reference Kotlin classes. Fix: In Maven, configure kotlin-maven-plugin to run in the `compile` phase before `maven-compiler-plugin`. In Gradle, this is handled automatically. [src7]
- **Mockito `any()` returns null in Kotlin**: Mockito's `any()` returns null, which crashes non-nullable Kotlin parameters. Fix: Use `mockito-kotlin` library (`org.mockito.kotlin:mockito-kotlin`) or switch to `mockk` which is Kotlin-native. [src3]
- **`@RequestParam` without nullable type**: In Java, `@RequestParam(required = false)` returns null. In Kotlin, the parameter must be declared nullable (`String?`) or have a default value; otherwise, Spring throws `MissingServletRequestParameterException`. Fix: Declare optional params as `name: String? = null` or `name: String = "default"`. [src7]
- **JSpecify compilation failures on Spring Boot 4.0 upgrade**: Spring Boot 4.0 adds JSpecify `@NullMarked` annotations across the entire API. Previously nullable-ambiguous platform types now have explicit nullability, which may cause compilation failures in Kotlin code that assumed non-null. Fix: Audit Spring API call sites and add `?` to parameters/return types that are now explicitly `@Nullable`. [src6, src8]

## Diagnostic Commands

```bash
# Verify Kotlin compiles alongside Java
./gradlew compileKotlin compileJava --info

# Check for remaining Java files (track migration progress)
find src/main -name "*.java" | wc -l
find src/main -name "*.kt" | wc -l

# Verify kotlin-spring plugin is opening classes correctly
./gradlew dependencies --configuration compileClasspath | grep kotlin

# Check for missing Jackson Kotlin module registration
./gradlew bootRun 2>&1 | grep -i "kotlin\|jackson"

# Run tests and check for Mockito null issues
./gradlew test --info 2>&1 | grep -i "NullPointerException\|UninitializedProperty"

# Verify JPA entity proxy creation (no final class errors)
./gradlew bootRun 2>&1 | grep -i "could not make.*final\|proxy\|cglib"

# Check Kotlin compiler version matches Spring Boot requirements
./gradlew kotlinCompilerVersion

# Verify kapt vs KSP status (should show no kapt tasks for KSP-migrated projects)
./gradlew tasks --all | grep -i "kapt\|ksp"
```

## Version History & Compatibility

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Spring Boot 4.0 (4.0.5, Mar 2026) + Kotlin 2.2 | Current GA (OSS support to 2026-12-31) | Kotlin 2.2.21 BOM baseline, JSpecify null-safety, org.springframework.lang.Nullable removed, spring-boot-starter-kotlinx-serialization-json added | Spring/Reactor/Micrometer types are null-aware in Kotlin; audit all API call sites for new nullability. Upgrade via 3.5 first. 4.1 due May 2026 (4.1.0-M4). [src6, src8, src9] |
| Spring Boot 3.5 (3.5.11, Feb 2026) + Kotlin 2.1/2.2 | Final 3.x minor, supported to 2026-06-30 | None | Recommended last 3.x stop before Spring Boot 4.0. Stable combination for new migrations |
| Spring Boot 3.4 + Kotlin 2.1 | EOL 2025-12-31 (final 3.4.13) | None | No longer receives OSS patches -- move to 3.5.11 or 4.0.x |
| Spring Boot 3.0 + Kotlin 1.9 | EOL 2023-12-31 | javax.* to jakarta.*, Java 17 minimum | Namespace migration required if upgrading from 2.x |
| Spring Boot 2.7 + Kotlin 1.8 | EOL 2023-06-30 (final 2.7.18) | Last javax.* support | Upgrade to 3.5.x before Kotlin migration |
| Kotlin 2.3 (2.3.20) | Latest stable (2026) | Continued K2 refinements | Compatible with Spring Boot 4.0; BOM pins 2.2.21 but newer 2.3.x can be set explicitly |
| Kotlin 2.0 | Stable (2024) | New K2 compiler (faster, stricter), kapt not enabled by default | K2 enabled by default; up to 94% faster compilation. Use `-Xjsr305=strict` |
| KSP (2.3.7) | Default annotation processor (2026) | Replaces kapt (now in maintenance mode) | Most libraries (Dagger, Room, Moshi) ship KSP artifacts. KSP and kapt can co-exist -- migrate module-by-module. |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Team wants null safety, coroutines, and concise syntax | Team has zero Kotlin experience and a critical deadline | Train first, migrate later |
| Starting new Spring Boot microservices | Codebase is scheduled for retirement within 12 months | Keep Java, invest in tests |
| Using Kotlin on Android and want shared language | Project is a library consumed by Java-only downstream teams | Stay Java or add `@JvmStatic`/`@JvmOverloads` everywhere |
| Eliminating Lombok dependency (maintenance burden) | Heavy use of annotation processors (MapStruct, Dagger) not yet KSP-compatible | Wait for KSP support or use kapt transitionally |
| Code reduction is a priority (20-40% fewer lines) | Team resists change and migration would cause friction | Introduce Kotlin in tests first |
| Upgrading to Spring Boot 4.0 (Kotlin is first-class) | Project uses Java 8 features extensively and cannot move to Java 17+ | Upgrade Java version first |

## Important Caveats

- IntelliJ's auto-converter (`Ctrl+Alt+Shift+K`) produces functional but non-idiomatic Kotlin. Plan for manual cleanup: replace `!!` with safe calls, convert streams to collection operators, introduce `when` expressions, and use expression body functions. Budget 2-3x the auto-conversion time for cleanup.
- Kotlin compilation is 10-30% slower than Java compilation in mixed projects. The K2 compiler (Kotlin 2.0+) significantly reduces this gap -- benchmarks show up to 94% faster clean builds and 376% faster incremental analysis compared to K1. Build caching and incremental compilation further mitigate the impact. [src2]
- Spring Boot 4.0 reached GA in November 2025; the current stable release is 4.0.5 (March 2026) and Spring Boot 4.1 is due May 2026 (4.1.0-M4 in milestone). It makes Kotlin 2.2 the baseline (BOM pins 2.2.21; 2.3.20 is the latest stable Kotlin) and adopts JSpecify null-safety across Spring, Reactor, and Micrometer -- there are no unsafe platform types in those APIs anymore, just Kotlin null-safe types. Kotlin callers that previously treated platform types as non-nullable may see new compiler warnings or errors after upgrading. The migration path is Spring Boot 3.5.11 -> 4.0. [src5, src6, src8, src9]
- Spring Boot 4.0 changes serialization defaults: when both Kotlin Serialization and Jackson are on the classpath, Kotlin Serialization handles only types annotated with `@Serializable` (at root or generic-type level) and everything else falls through to Jackson. Add the new `spring-boot-starter-kotlinx-serialization-json` starter if you want Kotlin Serialization as the primary mapper. [src9]
- Coroutines support in Spring WebFlux is mature, but Spring MVC (blocking) does not support `suspend` functions in controllers. Use WebFlux if coroutines are a primary motivation for adopting Kotlin. On Spring Boot 4.0, enable automatic coroutine context propagation with `spring.reactor.context-propagation=auto`. [src9]
- Code coverage metrics typically drop 5-15% after converting to Kotlin because generated `data class` methods (`copy`, `componentN`, `toString`) inflate the uncovered method count. Adjust coverage thresholds or exclude generated methods.
- 27% of Spring developers now use Kotlin (JetBrains/Spring 2025 survey), and the JetBrains-Spring strategic partnership ensures continued first-class Kotlin support in the Spring ecosystem. [src5]

## Related Units

- [Java EE to Spring Boot Migration](/software/migrations/java-ee-to-spring-boot/2026)
- [JavaScript to TypeScript Migration](/software/migrations/javascript-to-typescript/2026)
- [Python 2 to Python 3 Migration](/software/migrations/python2-to-python3/2026)
- [.NET Framework to .NET Core Migration](/software/migrations/dotnet-framework-to-core/2026)
