How do I define constructors and initialize class properties in Kotlin?

In Kotlin, you usually define constructors and initialize properties directly in the class header using a primary constructor.

1. Primary constructor

The most common style is:

class Person(val name: String, var age: Int)

This defines:

  • a class named Person
  • a read-only property name
  • a mutable property age
  • a constructor that requires both values

Usage:

fun main() {
    val person = Person("Alice", 30)

    println(person.name)
    println(person.age)

    person.age = 31
    println(person.age)
}

Here, val name: String and var age: Int are both constructor parameters and class properties.


2. Constructor parameters without properties

If you omit val or var, the parameter is only available during initialization:

class Person(name: String) {
    val uppercaseName = name.uppercase()
}

Usage:

fun main() {
    val person = Person("Alice")

    println(person.uppercaseName)
}

In this example, name is not a property. You cannot access person.name unless you declare it with val or var.


3. Initialize properties in the class body

You can initialize properties using constructor values:

class Rectangle(val width: Int, val height: Int) {
    val area: Int = width * height
}

Usage:

fun main() {
    val rectangle = Rectangle(5, 4)

    println(rectangle.area)
}

Output:

20

4. Use an init block

If you need validation or setup logic, use an init block:

class User(val username: String, val age: Int) {
    init {
        require(username.isNotBlank()) {
            "Username must not be blank"
        }

        require(age >= 0) {
            "Age must not be negative"
        }
    }
}

The init block runs when an object is created:

fun main() {
    val user = User("kotlinFan", 25)

    println(user.username)
}

5. Default constructor values

You can give constructor parameters default values:

class Product(
    val name: String,
    val price: Double = 0.0,
    val inStock: Boolean = true
)

Usage:

fun main() {
    val freeSample = Product("Sticker")
    val laptop = Product("Laptop", 999.99, false)

    println(freeSample.price)
    println(laptop.inStock)
}

6. Named arguments

Named arguments make constructor calls clearer:

class Book(
    val title: String,
    val author: String,
    val pages: Int
)

fun main() {
    val book = Book(
        title = "Kotlin Basics",
        author = "JetBrains",
        pages = 250
    )

    println(book.title)
}

7. Secondary constructors

Kotlin also supports secondary constructors, but they are less common:

class Car {
    val brand: String
    val year: Int

    constructor(brand: String, year: Int) {
        this.brand = brand
        this.year = year
    }
}

Usage:

fun main() {
    val car = Car("Toyota", 2024)

    println(car.brand)
    println(car.year)
}

However, this is usually better written with a primary constructor:

class Car(val brand: String, val year: Int)

8. Primary and secondary constructors together

If a class has a primary constructor, secondary constructors must delegate to it using this(...):

class Employee(val name: String, val role: String) {
    constructor(name: String) : this(name, "Employee")
}

Usage:

fun main() {
    val employee = Employee("Sam")
    val manager = Employee("Dana", "Manager")

    println(employee.role)
    println(manager.role)
}

9. Late initialization with lateinit

For mutable non-null properties initialized later, use lateinit var:

class Session {
    lateinit var token: String

    fun start(token: String) {
        this.token = token
    }
}

Usage:

fun main() {
    val session = Session()

    session.start("abc123")

    println(session.token)
}

Use lateinit carefully. Accessing it before initialization causes an exception.


10. Custom getters and setters

You can customize property access:

class Temperature(celsius: Double) {
    var celsius: Double = celsius
        set(value) {
            require(value >= -273.15) {
                "Temperature cannot be below absolute zero"
            }
            field = value
        }

    val fahrenheit: Double
        get() = celsius * 9 / 5 + 32
}

Usage:

fun main() {
    val temperature = Temperature(25.0)

    println(temperature.fahrenheit)

    temperature.celsius = 30.0
    println(temperature.fahrenheit)
}

Quick summary

class Person(
    val name: String,
    var age: Int = 0
) {
    init {
        require(name.isNotBlank()) {
            "Name cannot be blank"
        }
    }

    val isAdult: Boolean
        get() = age >= 18
}

This example shows:

  • val name: read-only property initialized from constructor
  • var age: mutable property with a default value
  • init: validation logic
  • isAdult: computed property

In most Kotlin code, prefer a primary constructor with val or var properties unless you specifically need more complex construction logic.

How do I create a class and an object in Kotlin?

In Kotlin, you create a class with the class keyword, and you create an object instance by calling the class constructor.

class Person {
    var name: String = "Unknown"
    var age: Int = 0
}

fun main() {
    val person = Person()

    person.name = "Alice"
    person.age = 25

    println("${person.name} is ${person.age} years old")
}

Output:

Alice is 25 years old

Class with a constructor

A more common Kotlin style is to define properties directly in the constructor:

class Person(
    val name: String,
    var age: Int
)

fun main() {
    val person = Person("Alice", 25)

    println("${person.name} is ${person.age} years old")
}

Here:

  • class Person(...) defines a class.
  • val name is a read-only property.
  • var age is a mutable property.
  • Person("Alice", 25) creates an object of the class.

Kotlin object keyword

Kotlin also has the object keyword, which creates a singleton object:

object AppConfig {
    val appName = "My Kotlin App"
    val version = "1.0"
}

fun main() {
    println(AppConfig.appName)
    println(AppConfig.version)
}

Unlike a class, you do not create instances of an object. There is only one instance, and you access it directly by name.

Java Class File Format Versions

A compiled Java .class file starts with a fixed header (0xCAFEBABE), followed by a pair of numbers: minor_version and major_version. The pair (commonly written as major.minor, e.g., 52.0) identifies which Java platform level the bytecode targets. The JVM uses this to decide whether it can load the class. If the class was compiled for a newer platform than the JVM supports, you’ll get UnsupportedClassVersionError.

Why It Matters:

  • Backward compatibility: Newer JVMs can generally run older class files, but not the other way around.
  • Build reproducibility: Ensuring all modules target the same release avoids subtle runtime issues.
  • Tooling alignment: IDEs, build tools, containers, and CI images must agree on the target level to prevent version skew.

Quick mapping highlights:

  • Java 8 → 52.0
  • Java 11 → 55.0
  • Java 17 (LTS) → 61.0
  • Java 21 (LTS) → 65.0
  • Java 22 → 66.0, 23 → 67.0, 24 → 68.0, 25 → 69.0, 26 → 70.0, 27 → 71.0, 28 → 72.0
JDK Version Class File Format Version
1.0 45.0
1.1 45.3
1.2 46.0
1.3 47.0
1.4 48.0
5 49.0
6 50.0
7 51.0
8 52.0
9 53.0
10 54.0
11 55.0
12 56.0
13 57.0
14 58.0
15 59.0
16 60.0
17 61.0
18 62.0
19 63.0
20 64.0
21 65.0
22 66.0
23 67.0
24 68.0
25 69.0
26 70.0
27 71.0
28 72.0

Note:

  • Early JDK branding used 1.x (e.g., 1.5, 1.6) but these correspond to modern names 5, 6, etc. The table above reflects the modern naming for 5+.
  • There was no official 1.9 brand; Java 9 is simply 9 → 53.0 (already shown above).

How to check a class file’s version

  • Using javap (JDK tool):
    javap -v path/to/Some.class | find "major"
    

    Look for a line like major version: NN (e.g., 52 for Java 8). For modern compilers, minor is typically 0.

  • Reading the header directly (forensics style):

    1. Confirm magic bytes: CA FE BA BE.
    2. Next 2 bytes: minor_version.
    3. Next 2 bytes: major_version (e.g., 0x003D = 61 → Java 17).

How to compile for a specific Java level

  • Recommended (single flag):
    javac --release 21 -d out $(find src -name "*.java")
    

    --release consistently sets language features, APIs, and the class file version.

  • Legacy approach (not preferred, can mismatch APIs):

    javac -source 1.8 -target 1.8 -bootclasspath "%JAVA8_HOME%\\jre\\lib\\rt.jar" -extdirs ""
    
  • Maven (maven-compiler-plugin):
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.11.0</version>
      <configuration>
          <release>21</release>
      </configuration>
    </plugin>
    
  • Gradle (Groovy DSL):
    java {
      toolchain {
          languageVersion = JavaLanguageVersion.of(21)
      }
    }
    // Or explicitly set the target bytecode
    tasks.withType(JavaCompile).configureEach {
      options.release = 21
    }
    

Common failure and how to fix

  • Symptom:
    • java.lang.UnsupportedClassVersionError: … has been compiled by a more recent version of the Java Runtime.
  • Causes:
    • Running on an older JRE/JDK than the class file requires.
    • Mixed toolchains or inconsistent --release/target levels in a multi-module build.
  • Fixes:
    • Upgrade the runtime to meet the class file’s major.minor level; or
    • Recompile with an older target using --release <level> that matches your deployment runtime; and
    • Standardize toolchains via Maven/Gradle toolchains and CI images to avoid skew.

Tips and caveats

  • Prefer --release over -source/-target because it also validates against platform APIs for that release.
  • Preview features do not change the class file version; they require --enable-preview at compile and run time, but the mapping still follows the JDK’s version.
  • When publishing libraries, choose the lowest --release that matches your supported runtime matrix to maximize compatibility; consider multi-release JARs if you need newer APIs while keeping a baseline.

How to Use Virtual Threads in Java (Project Loom) with Spring Boot

Virtual threads are one of the most exciting additions to modern Java. They make it much easier to write highly concurrent applications without the complexity of managing large thread pools, callbacks, or reactive pipelines.

If you build applications with Spring Boot, virtual threads can help your app handle many more concurrent tasks with simpler code.

In this post, we’ll cover:

  • What virtual threads are
  • Why they matter
  • When to use them
  • How to enable them in Spring Boot
  • A few important best practices and warnings

What are virtual threads?

Traditional Java threads are often called platform threads. They are mapped closely to operating system threads. That means they are relatively expensive in terms of memory and scheduling.

Virtual threads are lightweight threads managed by the JVM, not directly by the operating system. They are designed to make blocking code cheap again.

In simple terms:

  • Platform threads = heavier, fewer, more expensive
  • Virtual threads = lightweight, many more, cheaper to create

This means you can write code in the usual imperative style, but still support high concurrency.


Why virtual threads matter

For years, Java developers had two main choices for handling concurrency:

  1. Use thread pools and blocking code
  2. Use asynchronous/reactive programming

Virtual threads give you a third option:

  • Keep the simple blocking style
  • Avoid the complexity of reactive code
  • Scale better under lots of concurrent I/O operations

This is especially useful for applications that spend a lot of time waiting on:

  • Database calls
  • HTTP requests
  • File I/O
  • Remote service calls

If your application is mostly I/O-bound, virtual threads can be a great fit.


Virtual threads vs platform threads

Here’s a simple comparison:

Feature Platform Threads Virtual Threads
Cost to create Higher Very low
Memory usage Higher Lower
Number you can run Limited Much larger
Good for blocking code Yes, but expensive Yes, and efficient
Managed by OS Yes Mostly by JVM

The biggest win is that virtual threads let you run many blocking tasks concurrently without exhausting thread resources as quickly.


When should you use virtual threads?

Virtual threads are a strong choice when your app does lots of blocking I/O and you want simple code.

Good use cases:

  • REST APIs with many simultaneous requests
  • Service-to-service communication
  • Database-heavy applications
  • Background jobs that wait on I/O
  • File processing or batch operations

Less ideal use cases:

  • CPU-intensive tasks
  • Work that depends heavily on thread-local assumptions
  • Libraries that block while holding locks in a problematic way

Virtual threads do not magically make CPU-bound code faster. They help most when threads spend time waiting.


Project Loom in Java

Virtual threads are part of Project Loom, a long-running effort to modernize concurrency in Java.

Loom also introduced other improvements around structured concurrency and scoped values, but the headline feature most developers use today is virtual threads.

Virtual threads became a standard Java feature in recent releases, so you no longer need special preview settings in modern Java versions.


How Spring Boot supports virtual threads

Spring Boot has first-class support for virtual threads.

If you are using Spring Boot 3.2+, you can usually enable them with a simple configuration property:

spring.threads.virtual.enabled=true

That tells Spring Boot to use virtual threads in places where it manages request handling and task execution.

This is one of the nicest parts: in many cases, you can get the benefits of virtual threads with very little code change.


Basic setup in Spring Boot

1. Use a recent Java version

Virtual threads require a modern JDK, for example Java 25.

2. Use a recent Spring Boot version

Make sure your Spring Boot version supports virtual threads well. Spring Boot 3.2 or later is recommended.

3. Enable virtual threads

Add this to your application configuration:

spring.threads.virtual.enabled=true

Or in YAML:

spring:
  threads:
    virtual:
      enabled: true

That’s often enough for web applications.


What happens when you enable them?

When virtual threads are enabled, Spring can use them for tasks such as:

  • Handling incoming HTTP requests
  • Running @Async methods
  • Executing some scheduled or background tasks

The exact behavior depends on the Spring component and configuration, but the overall idea is that work can run on virtual threads instead of a small pool of platform threads.


Example: a Spring Boot REST controller

Here is a simple example of how your code can stay clean and blocking while still benefiting from virtual threads.

package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

    private final DemoService demoService;

    public DemoController(DemoService demoService) {
        this.demoService = demoService;
    }

    @GetMapping("/hello")
    public String hello() {
        return demoService.fetchMessage();
    }
}
package com.example.demo;

import org.springframework.stereotype.Service;

@Service
public class DemoService {

    public String fetchMessage() {
        // Simulate a blocking operation
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return "Hello from a virtual thread!";
    }
}

With virtual threads enabled, each request can be handled with a lightweight thread, even though the service method blocks.


Example: using virtual threads for background tasks

You can also create virtual threads manually when needed.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualThreadExample {

    public static void main(String[] args) {
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            executor.submit(() -> System.out.println("Task 1 on virtual thread"));
            executor.submit(() -> System.out.println("Task 2 on another virtual thread"));
        }
    }
}

This is useful when you want a simple executor that creates a new virtual thread for each task.


Using @Async with virtual threads

Spring’s @Async support can also benefit from virtual threads.

For example:

package com.example.demo;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.concurrent.CompletableFuture;

@Service
public class EmailService {

    @Async
    public CompletableFuture<String> sendEmail() {
        // Simulate work
        return CompletableFuture.completedFuture("Email sent");
    }
}

If Spring is configured to use virtual threads for task execution, these async methods can run on virtual threads, making blocking work much cheaper.


Virtual threads do not replace everything

It’s important not to oversell virtual threads. They solve one problem very well: cheap blocking concurrency.

They do not replace:

  • Proper database indexing
  • Efficient network design
  • Good application architecture
  • Caching
  • Load balancing
  • Performance tuning

Virtual threads are a concurrency tool, not a performance silver bullet.


Best practices when using virtual threads

1. Keep code simple

One of the main advantages of virtual threads is that you can keep using normal blocking code. Don’t add unnecessary complexity.

2. Avoid long synchronized blocks

Virtual threads can behave poorly if they spend too much time blocked inside synchronized sections. Prefer shorter critical sections and consider alternatives where appropriate.

3. Watch out for thread-local usage

Some older code relies heavily on ThreadLocal. Virtual threads support it, but large-scale usage can still create complexity and memory overhead.

4. Test your dependencies

Most modern libraries work well, but some older libraries may assume platform-thread behavior or may not play nicely with high concurrency.

5. Don’t use virtual threads for CPU-heavy work expecting miracles

If your bottleneck is CPU, virtual threads will not fix it. For CPU-bound tasks, focus on algorithms, parallelism, and profiling.


Common questions

Are virtual threads faster?

Not always in a direct “single task runs faster” sense. Their big advantage is that they allow more concurrency with less overhead, especially for blocking I/O.

Do I need reactive programming anymore?

Not necessarily. Virtual threads reduce the need for reactive programming in many applications, especially if you prefer imperative code.

Can I use virtual threads with Spring MVC?

Yes. Spring MVC is a great fit because it already uses a blocking request model, which maps naturally to virtual threads.

Can I use them with Spring WebFlux?

You can, but WebFlux is built around a reactive model. Virtual threads are often more valuable in traditional blocking stacks like Spring MVC.


When virtual threads are a great fit in Spring Boot

Virtual threads are especially attractive if:

  • You already have a Spring MVC application
  • You use JDBC and blocking database access
  • Your codebase is imperative and you want to keep it that way
  • You want to handle more concurrent requests with less thread-pool tuning

In many cases, virtual threads let you modernize your app without rewriting it.


A practical migration strategy

If you want to adopt virtual threads in an existing Spring Boot app, here’s a simple approach:

  1. Upgrade to a compatible Java and Spring Boot version
  2. Enable virtual threads in configuration
  3. Test your main request paths
  4. Watch application metrics under load
  5. Check library compatibility
  6. Tune only where needed

You usually do not need to rewrite your whole application.


Final thoughts

Virtual threads are a major step forward for Java concurrency. They make it possible to write straightforward, blocking-style code while still supporting high throughput and scalability.

For Spring Boot developers, this is especially valuable because it means:

  • Less concurrency boilerplate
  • Easier-to-read code
  • Better scaling for I/O-heavy workloads
  • A smoother path than fully reactive programming for many apps

If your application spends a lot of time waiting on I/O, virtual threads are absolutely worth trying.


Summary

Use virtual threads in Spring Boot when you want:

  • Simple blocking code
  • High concurrency
  • Less thread-pool management
  • Better scalability for I/O-bound workloads

Enable them with:

spring.threads.virtual.enabled=true

Then test, measure, and enjoy a much simpler concurrency model in Java.