Skip to content
CalliCoder

Kotlin Properties, Backing Fields, Getters and Setters

Kotlin 13 min read

A property is a pair of accessors, the backing field only exists when one is needed, why assigning to the property inside its own setter recurses forever, and the private-set pattern.

Kotlin has no fields in the Java sense. A property is a getter and, for a var, a setter; the field behind it is an implementation detail the compiler adds only when the accessors actually need storage. Once that is clear, the two common mistakes (infinite recursion in a setter, and a computed property recomputed on every read) both become obvious.

Written against Kotlin 1.9.

What a property generates

class User {
    var name: String = ""
    val id: Long = 0
}

That declares a private field plus a getter and setter for name, and a private field plus a getter for id. From Java it is getName(), setName(String) and getId(), the field is not accessible.

Which is why Kotlin has no need for the boilerplate a Java class of the same shape requires, and why adding a custom accessor later is not a breaking change: callers were always going through one.

Custom accessors

class Rectangle(val width: Double, val height: Double) {

    val area: Double
        get() = width * height

    var label: String = ""
        set(value) {
            field = value.trim()
        }
}

area has no backing field. Nothing is stored; the getter computes on each access, which is correct for something cheap and derived. If the computation is expensive. That is exactly the case by lazy exists for:

val checksum: String by lazy { computeChecksum() }

The rule the compiler follows: a backing field is generated only if at least one accessor uses the field identifier, or if the property uses the default implementation of either accessor.

field, and the recursion

field is available only inside an accessor and refers to the storage behind the property:

var label: String = ""
    set(value) {
        label = value.trim()    // infinite recursion — calls this setter again
    }

Assigning to the property name inside its own setter calls the setter, which assigns to the property name, and the result is a StackOverflowError. The same applies to reading the property inside its own getter.

field is the escape and the only way to reach the storage.

Validation in a setter

class Account {
    var balance: Long = 0
        set(value) {
            require(value >= 0) { "Balance cannot be negative: $value" }
            field = value
        }
}

That enforces the invariant on every assignment, including the ones made inside the class. Worth weighing against doing the check in a method: a setter that can throw makes assignment a fallible operation, which surprises callers who expect a.balance = x to be total.

The alternative that avoids the surprise is a private setter plus an explicit method:

class Account {
    var balance: Long = 0
        private set

    fun deposit(amount: Long) {
        require(amount > 0)
        balance += amount
    }
}

private set gives a public getter and a private setter, read-only from outside, mutable within. This is the most useful of the visibility forms and has no Java equivalent short of writing both accessors by hand.

The visibility modifier can only be applied to the setter, and only to make it more restrictive than the property.

Backing property, for a different external type

The common case is a collection that should be mutable inside and read-only outside:

class Cart {
    private val _items = mutableListOf<Item>()
    val items: List<Item> get() = _items

    fun add(item: Item) {
        _items += item
    }
}

items exposes a List, not a MutableList. It is the same object, so a caster can still reach the mutable interface: Collections.unmodifiableList(_items) or _items.toList() closes that at the cost of a wrapper or a copy.

The underscore prefix is the convention for a backing property in Kotlin, and it is worth keeping because it makes the pair obvious.

Note the get() = _items rather than = _items. With the second form, items would be initialised once to the same reference, which happens to behave identically for a mutable list and does not for anything that gets reassigned.

Late initialisation and delegation

class Service {
    lateinit var repository: Repository        // no backing field null, throws if read early
    val config: Config by lazy { load() }      // computed once, thread-safe
    var setting: String by Delegates.observable("") { _, old, new ->
        log.info("setting changed from $old to $new")
    }
}

A delegated property has no backing field of its own: the delegate object holds the state, and the generated accessors call getValue and setValue on it. by lazy, Delegates.observable, Delegates.vetoable and by map are the built-in ones, and writing a custom delegate is two methods.

lateinit is the one to use sparingly: it is a non-nullable property whose type system guarantee is enforced at runtime rather than compile time, which is the trade null safety usually argues against.

Constructor properties

class User(
    val id: Long,
    var name: String,
    private val secret: String,
)

val and var in the primary constructor declare properties, not just parameters. Dropping the keyword makes it a plain constructor parameter: usable in initialiser blocks and property initialisers, and not stored:

class User(name: String) {
    val displayName = name.uppercase()   // name is a parameter, not a property
}

That distinction matters for memory: a parameter without val is not retained.

Interoperating with Java and frameworks

Java sees accessors, not properties, and a few annotations exist because that translation is not always what a framework wants.

class Config {
    @JvmField
    val timeout = 30            // exposed as a public field, no getter

    @get:JvmName("isEnabled")
    val enabled = true          // getIsEnabled() would be the default for some names
}

@JvmField removes the accessors entirely and publishes the backing field. It is needed by the occasional library that reflects over fields, and it forfeits the ability to add a custom accessor later without breaking callers.

The use-site targets matter more in practice. An annotation on a property has several possible destinations: the property itself, its backing field, its getter, its setter, or a constructor parameter — and Kotlin picks one by a priority order that is often not the one a Java framework reads:

data class Request(
    @field:NotBlank val title: String,      // Bean Validation reads the field
    @get:JsonProperty("published_at") val publishedAt: Instant,
)

Without @field:, a validation annotation lands on the constructor parameter and is silently ignored. That failure has no error message of any kind, the validation simply never runs, and it is the single most common Kotlin-and-Spring problem.

A property named with an is prefix is the other interop wrinkle: val isEnabled generates isEnabled() rather than getIsEnabled(), which is what most Java tooling expects, and a property named enabled generates getEnabled().

Compile-time constants

const val MAX_RETRIES = 3          // top level or in an object; inlined at call sites
val timeout = Duration.ofSeconds(30)

const requires a primitive or String, a top-level or object/companion object location, and a compile-time-known value. Its value is copied into every call site, so changing it requires recompiling the callers, which is the one thing to be aware of across module boundaries.

Related: data classes and variables and data types. More in the Kotlin guides.

Frequently asked questions

Does Kotlin have fields?

Not as a language feature. A property is a getter and, for var, a setter; the compiler adds a backing field only when an accessor needs storage.

When is a backing field generated?

When an accessor uses the field identifier, or when the property relies on the default implementation of at least one accessor. A property with only a custom getter computing from other state has none.

Why does my setter cause a StackOverflowError?

It assigns to the property name, which calls the setter again. Assign to field instead. It is the only way to reach the storage.

Is field available outside an accessor?

No. It exists only inside the getter and setter of the property it belongs to.

How do I make a property read-only from outside but writable inside?

var x = ... with private set on the next line. Public getter, private setter.

Can I make the getter private and the setter public?

No. Only the setter can carry a visibility modifier, and only to be more restrictive than the property itself.

Should validation go in a setter?

It works, and it makes assignment fallible, which callers do not expect. A private setter plus an explicit method that validates is usually clearer.

What is a backing property?

A private mutable property plus a public read-only one exposing it — _items and items. It is how a MutableList is published as a List.

Does a computed property cache its value?

No, the getter runs on every access. Use by lazy when the computation is expensive and the value does not change.

What is the difference between val and const val?

val is read-only and assigned at runtime. const val is a compile-time constant, limited to primitives and strings, inlined into every call site.