KOTLIN: Executing Code During Construction — Init Blocks and Secondary Constructors

Kotlin provides two mechanisms for executing code automatically when an object is constructed — initializer blocks (init) and secondary constructors. Understanding when each runs and how they interact with the primary constructor is essential for building well-structured Kotlin classes.

A Kotlin class's primary constructor initializes properties, but sometimes you need to execute arbitrary logic during construction — validate inputs, log creation, register listeners, or set up state that depends on multiple properties. Kotlin provides two mechanisms for this: initializer blocks and secondary constructors.


The Construction Problem

A primary constructor is excellent at initializing properties, but it cannot contain arbitrary code — it exists only as a parameter list in the class header. When you need code to run during construction, you need a separate mechanism.

md
Class Construction Order
------------------------

1. Primary constructor parameters are evaluated
2. Properties are initialized (in order they appear)
3. init blocks run (in order they appear)
4. Secondary constructor body runs (if used)


Initializer Blocks

An initializer block is a block of code prefixed with the init keyword that runs as part of the primary constructor's execution. It runs every time an instance of the class is created, regardless of which constructor was used.

kotlin
class Person(val name: String, var weightLbs: Double) {

    init {
        println("Person created: $name, $weightLbs lbs")
        require(weightLbs > 0) { "Weight must be positive" }
    }

    var weightKilos: Double
        get() = weightLbs / 2.2
        set(value) { weightLbs = value * 2.2 }
}

When Person("Alice", 140.0) is called, the init block executes immediately after the properties name and weightLbs are assigned.

Key Characteristics of Initializer Blocks

  • Always runs during construction — every instance creation triggers all init blocks
  • Can have multiple init blocks — you can declare more than one in the same class
  • All init blocks run every time — if you have three init blocks, all three run for every instance
  • Can access constructor parametersname and weightLbs are available inside init
  • Runs in declaration order — if you have multiple init blocks, they execute top to bottom

kotlin
class MultiInit(val x: Int) {

    init {
        println("Init block 1: x = $x")  // Runs first
    }

    val doubled = x * 2  // Property initialized between init blocks

    init {
        println("Init block 2: doubled = $doubled")  // Runs second
    }
}

val obj = MultiInit(5)
// Output:
// Init block 1: x = 5
// Init block 2: doubled = 10

The second init block can use doubled because property initialization and init blocks run in the order they appear in the class body.

Common Uses for Init Blocks

  • Input validation — check constructor arguments and throw exceptions early
  • Logging — record that an object was created, for debugging
  • Initialization that depends on multiple properties — when the logic requires properties that are initialized separately
  • Side effects — registering the object with a registry, incrementing a counter, etc.

kotlin
class DatabaseConnection(val host: String, val port: Int) {

    init {
        require(port in 1..65535) { "Port must be between 1 and 65535" }
        require(host.isNotBlank()) { "Host must not be blank" }
        println("Connecting to $host:$port")
    }
}


Secondary Constructors

A secondary constructor is an additional constructor that provides an alternative way to create instances of the class. It is declared inside the class body using the constructor keyword.

kotlin
class Person(val name: String, var weightLbs: Double) {

    var weightKilos: Double
        get() = weightLbs / 2.2
        set(value) { weightLbs = value * 2.2 }

    // Secondary constructor: create a Person by specifying weight in kilos
    constructor(name: String, weightKilos: Double, unit: String) : this(name, weightKilos * 2.2) {
        println("Created $name from kilos: $weightKilos kg")
    }
}

Key Characteristics of Secondary Constructors

  • Runs only when used — unlike init blocks, a secondary constructor only runs if the caller invokes it
  • Can have multiple secondary constructors — each must have a distinct parameter signature
  • Must delegate to the primary constructor — if the class has a primary constructor, every secondary constructor must call it using this(...) (directly or indirectly)
  • Code runs after all initializer blocksinit blocks always run before the secondary constructor body

md
When secondary constructor is called:

1. Primary constructor parameters evaluated
2. Properties initialized
3. All init blocks run (in order)
4. Secondary constructor body runs

Delegation to Primary Constructor

The : this(...) syntax is mandatory when a primary constructor exists. A secondary constructor cannot bypass the primary constructor — it must route through it.

kotlin
class Config(val host: String, val port: Int) {

    init {
        println("Config init: $host:$port")
    }

    // Secondary constructor delegates to primary, then adds its own logic
    constructor(url: String) : this(
        host = url.substringBefore(":"),
        port = url.substringAfter(":").toInt()
    ) {
        // This body runs AFTER init block
        println("Parsed from URL: $url")
    }
}

val c = Config("localhost:8080")
// Output:
// Config init: localhost:8080
// Parsed from URL: localhost:8080


Init Blocks vs Secondary Constructors

Choosing between them is a matter of intent:

FeatureInit BlockSecondary Constructor
When it runsAlways, on every instance creationOnly when that specific constructor is called
Can have multipleYesYes
Runs after primary?As part of primaryAfter all init blocks
Access to constructor paramsYesVia delegation
Best forValidation, logging, setup for all instancesAlternative construction paths

When to Use Init Blocks

Use init when you need logic that every instance requires, regardless of how it was constructed:

  • Validating that constructor arguments meet preconditions
  • Logging or tracing object creation for debugging
  • Registering the object with a shared registry or listener list

When to Use Secondary Constructors

Use secondary constructors when you need alternative ways to construct an object:

  • Creating an object from a different representation (e.g., URL string vs host+port)
  • Providing convenience constructors for common use cases
  • Handling legacy API compatibility where callers may provide different parameter sets

In practice, default parameter values in the primary constructor eliminate many situations where secondary constructors would otherwise be needed:

kotlin
// Without secondary constructor — using default parameters
class HttpRequest(
    val url: String,
    val method: String = "GET",
    val timeout: Int = 30_000
)

// All of these work:
HttpRequest("https://example.com")
HttpRequest("https://example.com", "POST")
HttpRequest("https://example.com", "POST", 60_000)
HttpRequest(url = "https://example.com", timeout = 5_000)

Default parameters are usually cleaner and more flexible than secondary constructors for optional parameters. Reserve secondary constructors for cases where the construction logic genuinely differs in a meaningful way.


A Complete Example

kotlin
class NetworkClient(val baseUrl: String, val timeout: Int = 5000) {

    private val headers = mutableMapOf<String, String>()

    init {
        require(baseUrl.startsWith("http")) {
            "Base URL must start with http or https"
        }
        println("NetworkClient created for: $baseUrl")
    }

    init {
        // Second init block — runs after the first
        headers["Accept"] = "application/json"
        headers["User-Agent"] = "MyApp/1.0"
    }

    // Secondary constructor for pre-authenticated clients
    constructor(baseUrl: String, authToken: String) : this(baseUrl) {
        // This runs after both init blocks
        headers["Authorization"] = "Bearer $authToken"
        println("Client configured with auth token")
    }

    fun get(path: String): String {
        return "GET $baseUrl$path with timeout=$timeout"
    }
}

kotlin
// Using primary constructor
val client = NetworkClient("https://api.example.com")
// Output:
// NetworkClient created for: https://api.example.com
// (both init blocks run, headers populated)

// Using secondary constructor
val authClient = NetworkClient("https://api.example.com", "token123")
// Output:
// NetworkClient created for: https://api.example.com
// (both init blocks run)
// Client configured with auth token


Summary

ConceptPurpose
init blockCode that runs for every instance, as part of primary construction
Multiple init blocksAllowed; all run in declaration order
Secondary constructorAlternative construction path, declared with constructor keyword
: this(...) delegationRequired — secondary constructors must call the primary constructor
Execution orderProperties → init blocks → secondary constructor body

Understanding init blocks and secondary constructors gives you full control over what happens when your objects are created. Use init blocks for universal setup and validation, use secondary constructors for genuine alternative construction paths, and use default parameters to handle most cases where you might otherwise reach for a secondary constructor.

Construction logic should be predictable. Know exactly what runs, when, and in what order.