Primary and Secondary Constructors

A constructor is the code that runs when you create an instance of a class, and Kotlin gives you two flavors: a primary constructor baked directly into the class header, and one or more secondary constructors written as extra constructor(...) blocks inside the class body. Kotlin pushes you toward the primary constructor for the normal case — it is concise, pairs naturally with default parameter values, and eliminates most of the constructor-overloading boilerplate that Java classes accumulate. Secondary constructors still exist for the cases where you genuinely need multiple, differently-shaped ways to build an object, especially when interoperating with Java frameworks or when construction logic can’t be expressed as simple default values. This lesson covers both in depth: how they are declared, how init blocks fit in, the exact order everything runs in, and the mistakes that trip up newcomers.

Overview: How Constructors Work in Kotlin

In Java, a constructor is a special method-like block inside the class body. In Kotlin, the primary constructor is part of the class header itself — the parentheses right after the class name. class Person(val name: String, val age: Int) declares a class and its constructor in one line: no separate field declarations, no assignment statements, no boilerplate. If a constructor parameter is prefixed with val or var, Kotlin automatically turns it into a property of the class, readable (and, for var, writable) from outside. A parameter without val/var is just a plain constructor parameter — it exists only while the object is being built and is not exposed as a property afterward.

The primary constructor cannot contain executable code directly. Instead, any logic that needs to run during construction — validation, logging, computing a derived property — goes inside one or more init blocks. A class can have several init blocks, and the compiler runs them in the exact order they appear in the class body, interleaved with property initializers that also appear in that body. This ordering matters: an init block can see and use primary constructor parameters directly (they are in scope for the whole class body), but it can only see properties that were initialized above it.

Secondary constructors are declared with the constructor keyword inside the class body. If the class has a primary constructor, every secondary constructor must delegate to it — either directly with : this(...) or indirectly through another secondary constructor that eventually reaches the primary one. The compiler enforces this so that the primary constructor’s parameters, property initializers, and init blocks always run first, no matter which constructor the caller used. Only after that delegation completes does the body of the secondary constructor itself run. A class is also allowed to have no primary constructor at all and rely purely on secondary constructors, though this is uncommon in idiomatic Kotlin.

Aspect Primary constructor Secondary constructor
Where declared In the class header, e.g. class Foo(val x: Int) Inside the class body with the constructor keyword
Can hold executable code? No — use an init block instead Yes — the body after the delegation call
val/var parameters become properties? Yes, if marked val/var No — secondary constructor parameters are never properties
Delegation requirement None Must call the primary constructor (directly or indirectly) if one exists
Typical use The normal, everyday way to construct instances Alternate construction paths, Java interop, framework requirements

Syntax

The general shape of a class with both constructor types looks like this:

class ClassName(primaryParam1: Type1, val primaryParam2: Type2 = default2) {
    // property initializer -- runs in declaration order
    val computed: Type3 = primaryParam1.someTransform()

    // init block(s) -- run in declaration order, interleaved with
    // property initializers above and below them
    init {
        // has direct access to primary constructor parameters
    }

    // secondary constructor -- must delegate to the primary constructor
    constructor(other: Type4) : this(other.toType1(), default2) {
        // additional logic that only applies to this construction path
    }
}
  • primaryParam1 — a plain constructor parameter; usable inside the class body but not exposed as a property.
  • val primaryParam2 — becomes a read-only property; var would make it a mutable property instead.
  • = default2 — a default value, letting callers omit the argument entirely.
  • init { ... } — executable code that runs as part of construction, in source order relative to property initializers.
  • constructor(...) : this(...) — a secondary constructor; the part after the colon is the mandatory delegation call.

Examples

Example 1: A Basic Primary Constructor

The simplest and most common case — a class whose only job is to hold a couple of values.

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

fun main() {
    val person = Person("Ava", 30)
    println("${person.name} is ${person.age} years old")
}

Output:

Ava is 30 years old

There is no separate field declaration, no assignment statements, and no explicit constructor body — the class header does all of it. Because both parameters are declared with val, person.name and person.age are accessible as read-only properties from outside the class.

Example 2: Init Blocks and Default Parameters

This example computes a derived value in an init block and shows a default parameter in action.

class Rectangle(val width: Double, val height: Double = 1.0) {
    val area: Double

    init {
        area = width * height
        println("Created a rectangle of area $area")
    }
}

fun main() {
    val r1 = Rectangle(4.0, 5.0)
    val r2 = Rectangle(3.0)
    println("r1 area: ${r1.area}")
    println("r2 area: ${r2.area}")
}

Output:

Created a rectangle of area 20.0
Created a rectangle of area 3.0
r1 area: 20.0
r2 area: 3.0

area is declared without an initial value and then assigned inside init — the compiler tracks that this assignment happens exactly once before the object is usable, so it is legal even though area is a val. Because height has a default of 1.0, r2 can be constructed with a single argument. Notice the init block runs immediately as each object is built, so its println fires before either of the later two println calls.

Example 3: Secondary Constructors That Delegate

Here a secondary constructor adds an optional third piece of data on top of the primary constructor’s two required ones.

class Employee(val name: String, val salary: Double) {
    var department: String = "Unassigned"

    constructor(name: String, salary: Double, department: String) : this(name, salary) {
        this.department = department
    }
}

fun main() {
    val e1 = Employee("Sam", 55000.0)
    val e2 = Employee("Riya", 62000.0, "Engineering")
    println("${e1.name} works in ${e1.department}")
    println("${e2.name} works in ${e2.department}")
}

Output:

Sam works in Unassigned
Riya works in Engineering

The secondary constructor’s : this(name, salary) call runs the primary constructor first (setting name and salary, initializing department to "Unassigned"), and only then does the secondary constructor’s own body run and overwrite department. department is declared with var because, unlike name and salary, it genuinely needs to be reassigned after the object exists.

How It Works Step by Step

To see the delegation chain clearly, consider a class with no primary constructor at all — just two secondary constructors, one delegating to the other.

class Logger {
    val tag: String

    constructor(tag: String) {
        this.tag = tag
        println("Logger created with tag $tag")
    }

    constructor() : this("DEFAULT")
}

fun main() {
    val l1 = Logger("Network")
    val l2 = Logger()
    println("${l1.tag}, ${l2.tag}")
}

Output:

Logger created with tag Network
Logger created with tag DEFAULT
Network, DEFAULT
  1. Logger("Network") calls the constructor(tag: String) overload directly. It sets tag and prints the message.
  2. Logger() calls the no-argument constructor. Before its (empty) body can run, Kotlin evaluates its delegation clause, : this("DEFAULT"), which invokes constructor(tag: String) with "DEFAULT".
  3. That delegated call sets tag = "DEFAULT" and prints its own message — this is why the second println appears before Logger()‘s own (empty) body finishes.
  4. Only after both objects exist does main print the final combined line.

This is the general rule: whichever constructor you call, Kotlin always walks the delegation chain down to wherever the real initialization work happens before anything else in that constructor’s own body executes.

Common Mistakes

Mistake 1: Forgetting a secondary constructor must delegate

If a class has a primary constructor, every secondary constructor must call it (directly or through another secondary constructor). Skipping this does not compile.

class Point(val x: Int, val y: Int) {
    constructor(x: Int) {
        // Error: this secondary constructor never delegates
        // to the primary constructor -- required whenever a
        // primary constructor exists.
        println(x)
    }
}

The fix is to add the delegation call after the colon:

class Point(val x: Int, val y: Int) {
    constructor(x: Int) : this(x, 0)

    override fun toString(): String = "($x, $y)"
}

fun main() {
    val p1 = Point(3, 4)
    val p2 = Point(5)
    println(p1)
    println(p2)
}

Output:

(3, 4)
(5, 0)

Mistake 2: Forgetting val/var on a constructor parameter

A primary constructor parameter is only turned into a property if it is marked val or var. Without that keyword, it is just a local parameter usable inside the class body, and any attempt to access it from outside is an unresolved reference.

class Coordinate(x: Int, y: Int) {
    // x and y here are plain constructor parameters,
    // not properties -- they don't exist outside init/the body.
}

fun main() {
    val c = Coordinate(1, 2)
    println(c.x) // Error: unresolved reference 'x'
}

Adding val makes them real, readable properties:

class Coordinate(val x: Int, val y: Int)

fun main() {
    val c = Coordinate(1, 2)
    println("(${c.x}, ${c.y})")
}

Output:

(1, 2)

Mistake 3: Reaching for secondary constructors when default parameters would do

Programmers coming from Java often reach for a stack of secondary constructors to simulate overloading, when Kotlin’s default and named parameters already solve this more concisely.

class Message(val text: String, val sender: String) {
    constructor(text: String) : this(text, "Unknown")
    constructor() : this("", "Unknown")
}

fun main() {
    val m1 = Message("Hello", "Ava")
    val m2 = Message("Hi")
    val m3 = Message()
    println("${m1.sender}: ${m1.text}")
    println("${m2.sender}: ${m2.text}")
    println("${m3.sender}: ${m3.text}")
}

Output:

Ava: Hello
Unknown: Hi
Unknown: 

This compiles and works, but it is three constructors’ worth of ceremony for something a single primary constructor with defaults expresses directly:

class Message(val text: String = "", val sender: String = "Unknown")

fun main() {
    val m1 = Message("Hello", "Ava")
    val m2 = Message("Hi")
    val m3 = Message()
    println("${m1.sender}: ${m1.text}")
    println("${m2.sender}: ${m2.text}")
    println("${m3.sender}: ${m3.text}")
}

Output:

Ava: Hello
Unknown: Hi
Unknown: 

Same behavior, one constructor. Reserve secondary constructors for cases default parameters genuinely can’t express, such as adapting a Java framework’s required constructor signature or building from an incompatible source type.

Best Practices

  • Prefer the primary constructor with default and named parameters over secondary constructors whenever the alternate “shapes” can be expressed as optional values.
  • Mark primary constructor parameters val by default; only use var when the property genuinely needs to change after construction.
  • Keep init blocks focused on validation or derived-value computation; avoid scattering many small init blocks that make the initialization order hard to follow.
  • Remember a val constructor parameter is a read-only reference, not deep immutability — a val holding a MutableList can still have its contents changed.
  • Use secondary constructors mainly for Java interop or when a class needs to be built from an incompatible source without duplicating primary-constructor logic.
  • Validate constructor arguments early with require() or check() inside an init block so invalid objects can never be constructed in the first place.

Practice Exercises

  • Write a data class Circle(val radius: Double) with an init block that computes and stores area (Math.PI * radius * radius). Print the area for a circle of radius 2.0.
  • Write a class Book(val title: String, val author: String, val year: Int = 2024) and add a secondary constructor Book(title: String) that delegates to the primary constructor using "Unknown" as the author. Construct one book with all three arguments and one with just a title, then print both.
  • Write a class Temperature with no primary constructor and two secondary constructors: one that takes a Celsius Double directly, and one that takes a Fahrenheit Double and converts it to Celsius before delegating to the first. Print the stored Celsius value for both construction paths.

Summary

  • The primary constructor lives in the class header; val/var parameters there become properties, plain parameters do not.
  • Primary constructors cannot contain code directly — use one or more init blocks, which run in the order they appear, interleaved with property initializers.
  • Secondary constructors are declared with the constructor keyword inside the class body and, if a primary constructor exists, must delegate to it directly or indirectly with : this(...).
  • Delegation always runs before a secondary constructor’s own body, guaranteeing the primary constructor’s setup always happens first.
  • Default parameter values usually replace the need for multiple secondary constructors and are the more idiomatic Kotlin choice.