How do I implement the singleton pattern idiomatically using Kotlin’s object declaration?

In Kotlin, the idiomatic way to implement the Singleton pattern is to use an object declaration.

object AppConfig {
    val appName = "MyApp"

    fun printConfig() {
        println("App name: $appName")
    }
}

You use it directly by its name:

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

Kotlin guarantees that an object declaration:

  • has exactly one instance
  • is initialized lazily, when first accessed
  • is thread-safe
  • can contain properties, functions, initialization blocks, and implement interfaces

Example with initialization:

object DatabaseManager {
    init {
        println("DatabaseManager initialized")
    }

    fun connect() {
        println("Connecting to database...")
    }
}

Usage:

fun main() {
    DatabaseManager.connect()
}

If you need a singleton that implements an interface:

interface Logger {
    fun log(message: String)
}

object ConsoleLogger : Logger {
    override fun log(message: String) {
        println("[LOG] $message")
    }
}

Usage:

fun main() {
    ConsoleLogger.log("Application started")
}

So instead of writing a private constructor and static getInstance() method like in Java, Kotlin’s idiomatic singleton is simply:

object MySingleton {
    fun doSomething() {
        println("Doing something")
    }
}

How do I mix OOP with functional programming using Kotlin’s design patterns?

You can mix OOP and functional programming in Kotlin by using objects/classes to model state, identity, boundaries, and domain concepts, while using functions/lambdas to model behavior, transformation, policies, and workflows.

Kotlin is especially good at this because it supports:

  • Classes, interfaces, inheritance, encapsulation
  • Data classes and sealed classes
  • Lambdas and higher-order functions
  • Immutability with val
  • Extension functions
  • Scope functions like let, run, also, apply
  • Collection pipelines like map, filter, fold

1. Use OOP for domain models, FP for transformations

A common Kotlin style is to model entities with classes, then transform them using pure functions.

data class User(
    val id: Long,
    val name: String,
    val age: Int,
    val isActive: Boolean
)

fun User.canReceivePromotion(): Boolean =
    isActive && age >= 18

fun promoteEligibleUsers(users: List<User>): List<User> =
    users.filter { it.canReceivePromotion() }

Here:

  • User is an OOP-style domain object.
  • canReceivePromotion is behavior attached through an extension function.
  • filter expresses functional transformation over a collection.

This avoids writing procedural loops like:

val result = mutableListOf<User>()
for (user in users) {
    if (user.isActive && user.age >= 18) {
        result.add(user)
    }
}

Prefer:

val result = users.filter { it.isActive && it.age >= 18 }

2. Prefer immutable objects

Functional programming favors immutability. In Kotlin, use val and data class.copy().

data class Order(
    val id: Long,
    val status: OrderStatus,
    val total: Double
)

enum class OrderStatus {
    Draft,
    Paid,
    Shipped,
    Cancelled
}

fun markAsPaid(order: Order): Order =
    order.copy(status = OrderStatus.Paid)

Instead of mutating the existing object:

class MutableOrder {
    var status: OrderStatus = OrderStatus.Draft
}

Prefer creating a new value:

val paidOrder = order.copy(status = OrderStatus.Paid)

This makes code easier to test, reason about, and safely use in concurrent contexts.


3. Use interfaces plus lambdas for Strategy pattern

The classic OOP Strategy pattern uses an interface:

interface DiscountStrategy {
    fun apply(total: Double): Double
}

class NoDiscount : DiscountStrategy {
    override fun apply(total: Double): Double = total
}

class PercentageDiscount(
    private val percent: Double
) : DiscountStrategy {
    override fun apply(total: Double): Double =
        total * (1 - percent)
}

In Kotlin, if the behavior is simple, you can replace the interface with a function type:

typealias DiscountStrategy = (Double) -> Double

val noDiscount: DiscountStrategy = { total -> total }

fun percentageDiscount(percent: Double): DiscountStrategy =
    { total -> total * (1 - percent) }

class CheckoutService(
    private val discountStrategy: DiscountStrategy
) {
    fun checkout(total: Double): Double =
        discountStrategy(total)
}

Usage:

val service = CheckoutService(
    discountStrategy = percentageDiscount(0.15)
)

val finalTotal = service.checkout(100.0)
println(finalTotal) // 85.0

This is a good example of mixing both styles:

  • CheckoutService is object-oriented.
  • DiscountStrategy is functional.
  • The behavior is injected as a function.

Use an interface when the strategy has multiple methods or meaningful identity. Use a function type when it is just one operation.


4. Use sealed classes for type-safe state and results

Instead of nullable values, error codes, or large inheritance hierarchies, Kotlin often uses sealed classes.

sealed class PaymentResult {
    data class Success(val transactionId: String) : PaymentResult()
    data class Failure(val reason: String) : PaymentResult()
    data object Pending : PaymentResult()
}

Then handle all possible cases with when:

fun describe(result: PaymentResult): String =
    when (result) {
        is PaymentResult.Success ->
            "Payment completed: ${result.transactionId}"

        is PaymentResult.Failure ->
            "Payment failed: ${result.reason}"

        PaymentResult.Pending ->
            "Payment is pending"
    }

This combines:

  • OOP-style polymorphic modeling
  • FP-style expression-based branching
  • Compile-time exhaustiveness checking

This is often cleaner than:

class PaymentResult(
    val success: Boolean,
    val errorMessage: String?,
    val transactionId: String?
)

because that structure can represent invalid states.


5. Replace some Template Method patterns with higher-order functions

Classic OOP Template Method might look like this:

abstract class ReportGenerator {
    fun generate(): String {
        val data = loadData()
        val formatted = format(data)
        return export(formatted)
    }

    protected abstract fun loadData(): List<String>
    protected abstract fun format(data: List<String>): String
    protected abstract fun export(content: String): String
}

In Kotlin, you can often use higher-order functions:

class ReportGenerator(
    private val loadData: () -> List<String>,
    private val format: (List<String>) -> String,
    private val export: (String) -> String
) {
    fun generate(): String {
        val data = loadData()
        val formatted = format(data)
        return export(formatted)
    }
}

Usage:

val generator = ReportGenerator(
    loadData = { listOf("A", "B", "C") },
    format = { data -> data.joinToString(separator = "\n") },
    export = { content -> "Report:\n$content" }
)

println(generator.generate())

This avoids subclassing when all you need is customizable behavior.


6. Use composition over inheritance

Kotlin works well when you compose small behaviors instead of building deep inheritance trees.

interface Logger {
    fun log(message: String)
}

class ConsoleLogger : Logger {
    override fun log(message: String) {
        println(message)
    }
}

class UserService(
    private val logger: Logger,
    private val validateUser: (User) -> Boolean
) {
    fun register(user: User) {
        if (!validateUser(user)) {
            logger.log("Invalid user: ${user.name}")
            return
        }

        logger.log("Registered user: ${user.name}")
    }
}

Here:

  • Logger is an OOP abstraction.
  • validateUser is a functional dependency.
  • UserService composes both.

Usage:

val service = UserService(
    logger = ConsoleLogger(),
    validateUser = { user -> user.age >= 18 && user.isActive }
)

This is flexible without excessive inheritance.


7. Use extension functions to add behavior without modifying classes

Extension functions are useful when you want functional-style transformations around OOP models.

data class Product(
    val name: String,
    val price: Double,
    val category: String
)

fun Product.withTax(rate: Double): Product =
    copy(price = price * (1 + rate))

fun Product.isExpensive(): Boolean =
    price > 100.0

Usage:

val products = listOf(
    Product("Keyboard", 80.0, "Electronics"),
    Product("Monitor", 250.0, "Electronics")
)

val taxedExpensiveProducts =
    products
        .map { it.withTax(0.2) }
        .filter { it.isExpensive() }

This keeps your domain model clean while allowing expressive pipelines.


8. Use functional pipelines inside object-oriented services

You do not need to choose between “service classes” and functional pipelines. They combine naturally.

class InvoiceService {
    fun calculateTotal(items: List<InvoiceItem>): Double =
        items
            .filter { it.quantity > 0 }
            .map { it.price * it.quantity }
            .sum()
}

data class InvoiceItem(
    val name: String,
    val price: Double,
    val quantity: Int
)

The class defines the business capability. The method implementation uses functional collection operations.


9. Use command objects or lambdas for Command pattern

Classic OOP Command pattern:

interface Command {
    fun execute()
}

class PrintCommand(
    private val message: String
) : Command {
    override fun execute() {
        println(message)
    }
}

Kotlin functional version:

typealias Command = () -> Unit

val printCommand: Command = {
    println("Hello from command")
}

fun runCommand(command: Command) {
    command()
}

Usage:

runCommand {
    println("Executing inline command")
}

Use a class-based command when the command has state, metadata, undo behavior, or lifecycle. Use a lambda when it is just executable behavior.


10. Use Repository as OOP boundary, FP for business rules

A practical architecture pattern is:

  • Repositories/gateways: OOP interfaces
  • Use cases/services: classes
  • Business rules: pure functions
  • Data transformations: functional pipelines
interface UserRepository {
    fun findAll(): List<User>
    fun save(user: User)
}

class ActivateUsersUseCase(
    private val repository: UserRepository
) {
    fun execute() {
        repository
            .findAll()
            .filter { shouldActivate(it) }
            .map { it.copy(isActive = true) }
            .forEach { repository.save(it) }
    }

    private fun shouldActivate(user: User): Boolean =
        !user.isActive && user.age >= 18
}

This gives you:

  • Testable boundaries
  • Clear dependency injection
  • Pure business logic where possible
  • OOP structure where useful

11. Map design patterns to Kotlin idioms

Many GoF-style design patterns become simpler in Kotlin.

Classic Pattern Kotlin-Friendly Approach
Strategy Function type, lambda, or interface
Command () -> Unit or command class
Factory Top-level function, companion object, or lambda
Template Method Higher-order function composition
Decorator Composition, extension functions, wrapper classes
Observer Function callbacks, Flow, listener interfaces
State Sealed classes plus when
Visitor Sealed classes plus exhaustive when
Builder Named/default arguments, DSL builders
Adapter Extension functions or wrapper classes

Example factory:

sealed class Notification {
    data class Email(val address: String) : Notification()
    data class Sms(val phoneNumber: String) : Notification()
}

fun createNotification(type: String, target: String): Notification =
    when (type) {
        "email" -> Notification.Email(target)
        "sms" -> Notification.Sms(target)
        else -> error("Unsupported notification type: $type")
    }

12. A practical mixed-style example

data class CartItem(
    val name: String,
    val price: Double,
    val quantity: Int
)

data class Cart(
    val items: List<CartItem>
)

typealias PricingRule = (CartItem) -> Double

class CartCalculator(
    private val pricingRule: PricingRule
) {
    fun total(cart: Cart): Double =
        cart.items.sumOf { item ->
            pricingRule(item) * item.quantity
        }
}

fun standardPricing(item: CartItem): Double =
    item.price

fun discountedPricing(discount: Double): PricingRule =
    { item -> item.price * (1 - discount) }

Usage:

val cart = Cart(
    listOf(
        CartItem("Book", 20.0, 2),
        CartItem("Pen", 2.0, 5)
    )
)

val calculator = CartCalculator(
    pricingRule = discountedPricing(0.10)
)

println(calculator.total(cart)) // 45.0

What this demonstrates:

  • Cart and CartItem are OOP/value models.
  • PricingRule is a functional strategy.
  • CartCalculator is an object-oriented service.
  • The total calculation uses functional collection operations.

General guidelines

Use OOP when you need:

  • Domain concepts with identity
  • Encapsulation
  • Long-lived services
  • Polymorphic boundaries
  • Dependency injection
  • External system boundaries like repositories, APIs, databases

Use functional programming when you need:

  • Data transformations
  • Stateless business rules
  • Reusable policies
  • Collection processing
  • Validation
  • Pipelines
  • Behavior injection

A good Kotlin rule of thumb:

Model your domain with objects, model your behavior with functions, and keep state immutable unless mutation is clearly necessary.

That gives you Kotlin code that is expressive, testable, and maintainable.

How do I use type aliases to simplify complex class structures in Kotlin?

In Kotlin, type aliases let you give a shorter, more meaningful name to an existing type. They are especially useful when your code has deeply nested generics, function types, or repeated complex class structures.

A type alias does not create a new type. It only creates an alternative name for an existing type.

Basic syntax

typealias AliasName = ExistingType

Example:

typealias UserId = String

fun findUser(id: UserId) {
    println("Finding user with id: $id")
}

Here, UserId is still just a String, but the code is more expressive.


Simplifying complex generic types

Suppose you have a complex nested structure:

val permissions: Map<String, List<Pair<String, Boolean>>> = mapOf(
    "admin" to listOf("delete" to true, "edit" to true),
    "guest" to listOf("view" to true)
)

You can simplify it with type aliases:

typealias PermissionName = String
typealias IsAllowed = Boolean
typealias Permission = Pair<PermissionName, IsAllowed>
typealias RolePermissions = Map<String, List<Permission>>

val permissions: RolePermissions = mapOf(
    "admin" to listOf("delete" to true, "edit" to true),
    "guest" to listOf("view" to true)
)

This makes the purpose of each part clearer.


Simplifying nested class structures

Type aliases are useful when referencing nested classes:

class ApiResponse {
    class Metadata {
        class Pagination {
            data class PageInfo(
                val page: Int,
                val size: Int,
                val total: Int
            )
        }
    }
}

typealias PageInfo = ApiResponse.Metadata.Pagination.PageInfo

fun printPageInfo(info: PageInfo) {
    println("Page ${info.page} of size ${info.size}")
}

Instead of writing:

ApiResponse.Metadata.Pagination.PageInfo

everywhere, you can use:

PageInfo

Simplifying function types

Type aliases are very common for callbacks and handlers:

typealias SuccessCallback<T> = (T) -> Unit
typealias ErrorCallback = (Throwable) -> Unit

fun <T> loadData(
    onSuccess: SuccessCallback<T>,
    onError: ErrorCallback
) {
    try {
        // Load data
    } catch (e: Throwable) {
        onError(e)
    }
}

This is easier to read than:

fun <T> loadData(
    onSuccess: (T) -> Unit,
    onError: (Throwable) -> Unit
)

Simplifying collection-heavy models

For example, instead of repeatedly writing:

Map<String, MutableList<Map<String, Any?>>>

you can define:

typealias JsonObject = Map<String, Any?>
typealias JsonObjectList = MutableList<JsonObject>
typealias GroupedJsonObjects = Map<String, JsonObjectList>

Then use:

fun process(data: GroupedJsonObjects) {
    // ...
}

Type aliases with generic parameters

Type aliases can also be generic:

typealias ResultHandler<T> = (Result<T>) -> Unit

fun fetchUser(handler: ResultHandler<User>) {
    // ...
}

Another example:

typealias StringMap<T> = Map<String, T>

val userAges: StringMap<Int> = mapOf(
    "Alice" to 30,
    "Bob" to 25
)

Important limitation: aliases are not new types

This is valid:

typealias UserId = String
typealias ProductId = String

fun loadUser(id: UserId) {
    println(id)
}

val productId: ProductId = "p-123"

loadUser(productId)

Even though ProductId and UserId have different alias names, both are still String.

If you need real type safety, use a value class instead:

@JvmInline
value class UserId(val value: String)

@JvmInline
value class ProductId(val value: String)

Now UserId and ProductId are distinct types.


When to use type aliases

Use type aliases when you want to:

  • Shorten long generic types
  • Give semantic names to data structures
  • Improve readability of callback/function types
  • Simplify references to nested classes
  • Avoid repeating verbose type declarations

Avoid using them when:

  • You need a truly distinct type
  • The alias hides important complexity
  • The alias name is vague, like Data, Info, or Thing

Example: before and after

Before:

class EventBus {
    private val listeners: MutableMap<String, MutableList<(Map<String, Any?>) -> Unit>> =
        mutableMapOf()

    fun subscribe(event: String, listener: (Map<String, Any?>) -> Unit) {
        listeners.getOrPut(event) { mutableListOf() }.add(listener)
    }

    fun publish(event: String, payload: Map<String, Any?>) {
        listeners[event]?.forEach { listener ->
            listener(payload)
        }
    }
}

After:

typealias EventName = String
typealias EventPayload = Map<String, Any?>
typealias EventListener = (EventPayload) -> Unit
typealias ListenerRegistry = MutableMap<EventName, MutableList<EventListener>>

class EventBus {
    private val listeners: ListenerRegistry = mutableMapOf()

    fun subscribe(event: EventName, listener: EventListener) {
        listeners.getOrPut(event) { mutableListOf() }.add(listener)
    }

    fun publish(event: EventName, payload: EventPayload) {
        listeners[event]?.forEach { listener ->
            listener(payload)
        }
    }
}

The runtime behavior is the same, but the structure is easier to understand.

In short: use typealias to make complex Kotlin types easier to read, but use value classes when you need stronger type safety.

How do I use the this, super, and @ labels for disambiguation in Kotlin class hierarchies?

In Kotlin, this, super, and @ labels are used to disambiguate which receiver or superclass member you mean, especially in nested scopes, inheritance, and inner classes.

1. this: refer to the current receiver

Inside a class, this refers to the current instance of that class.

class User(val name: String) {
    fun printName() {
        println(this.name)
    }
}

Usually this is optional:

println(name)

is the same as:

println(this.name)

2. this@Label: choose a specific outer receiver

When you have nested classes, lambdas, or extension functions, there may be multiple possible this receivers. Kotlin lets you qualify this with a label.

Class receiver

class Outer {
    val name = "Outer"

    inner class Inner {
        val name = "Inner"

        fun printNames() {
            println(this.name)        // Inner
            println([email protected])  // Inner
            println([email protected])  // Outer
        }
    }
}

this@Outer explicitly means “the this of Outer”.


3. Labels in lambdas

You can label lambdas and then use this@label to access that lambda’s receiver.

class Html {
    fun body() {
        println("body")
    }
}

fun html(block: Html.() -> Unit) {
    Html().block()
}

fun main() {
    html outer@ {
        this.body()
        [email protected]()
    }
}

Here:

this@outer

refers to the receiver of the lambda labeled outer.


4. super: call superclass implementation

Use super to access a member from the immediate superclass.

open class Parent {
    open fun greet() {
        println("Hello from Parent")
    }
}

class Child : Parent() {
    override fun greet() {
        super.greet()
        println("Hello from Child")
    }
}

Output:

Hello from Parent
Hello from Child

5. super<Type>: disambiguate multiple inherited implementations

If a class inherits the same member from multiple supertypes, you must specify which one to call.

interface A {
    fun greet() {
        println("Hello from A")
    }
}

interface B {
    fun greet() {
        println("Hello from B")
    }
}

class C : A, B {
    override fun greet() {
        super<A>.greet()
        super<B>.greet()
        println("Hello from C")
    }
}

Here:

super<A>.greet()
super<B>.greet()

select the specific supertype implementation.


6. super@Label: access an outer class’s superclass

In inner classes, super normally refers to the superclass of the inner class. If you need the superclass of an outer class, use a qualified super.

open class Base {
    open fun message() {
        println("Base")
    }
}

open class OuterBase : Base() {
    override fun message() {
        println("OuterBase")
    }
}

class Outer : OuterBase() {
    override fun message() {
        println("Outer")
    }

    inner class Inner {
        fun callOuterSuper() {
            [email protected]()
        }
    }
}

Here:

[email protected]()

means “call the superclass implementation of Outer”.

So this calls:

OuterBase.message()

not Outer.message().


7. Combining super<Type>@Label

If the outer class implements multiple supertypes, you can combine both forms.

interface A {
    fun print() {
        println("A")
    }
}

interface B {
    fun print() {
        println("B")
    }
}

class Outer : A, B {
    override fun print() {
        println("Outer")
    }

    inner class Inner {
        fun callOuterSupers() {
            super<A>@Outer.print()
            super<B>@Outer.print()
        }
    }
}

Here:

super<A>@Outer.print()
super<B>@Outer.print()

means:

  • call A’s implementation as inherited by Outer
  • call B’s implementation as inherited by Outer

Summary

Syntax Meaning
this Current receiver
this@Outer this of a specific labeled or outer receiver
super Immediate superclass implementation
super<Type> Specific superclass or interface implementation
super@Outer Superclass implementation of an outer class
super<Type>@Outer Specific supertype implementation of an outer class

In short:

this@Something

chooses which object/receiver you mean.

super<Something>

chooses which superclass/interface implementation you mean.

super<Something>@Outer

chooses which supertype implementation of which outer receiver you mean.

How do I use delegation by `by` keyword to implement interfaces or properties in Kotlin?

In Kotlin, the by keyword is used for delegation. It lets one object delegate behavior to another object instead of implementing everything manually.

There are two common forms:

  1. Interface/class delegation
  2. Property delegation

1. Interface delegation

If a class implements an interface, it can delegate the implementation of that interface to another object using by.

Example

interface Printer {
    fun print(message: String)
}

class ConsolePrinter : Printer {
    override fun print(message: String) {
        println(message)
    }
}

class LoggingPrinter(
    private val printer: Printer
) : Printer by printer

Here, LoggingPrinter implements Printer, but the actual implementation is delegated to printer.

Usage:

fun main() {
    val printer = LoggingPrinter(ConsolePrinter())
    printer.print("Hello Kotlin")
}

Output:

Hello Kotlin

LoggingPrinter does not need to manually implement print() because Printer by printer forwards calls to the given printer.


Overriding delegated behavior

You can still override specific methods if you want custom behavior.

interface Printer {
    fun print(message: String)
}

class ConsolePrinter : Printer {
    override fun print(message: String) {
        println(message)
    }
}

class LoggingPrinter(
    private val printer: Printer
) : Printer by printer {
    override fun print(message: String) {
        println("Logging message: $message")
        printer.print(message)
    }
}

Now calls to print() use the overridden method in LoggingPrinter.

fun main() {
    val printer = LoggingPrinter(ConsolePrinter())
    printer.print("Hello Kotlin")
}

Output:

Logging message: Hello Kotlin
Hello Kotlin

2. Delegating multiple interfaces

A class can delegate multiple interfaces to different objects.

interface Reader {
    fun read(): String
}

interface Writer {
    fun write(value: String)
}

class FileReader : Reader {
    override fun read(): String = "file contents"
}

class ConsoleWriter : Writer {
    override fun write(value: String) {
        println(value)
    }
}

class FileProcessor(
    reader: Reader,
    writer: Writer
) : Reader by reader, Writer by writer

Usage:

fun main() {
    val processor = FileProcessor(FileReader(), ConsoleWriter())

    val text = processor.read()
    processor.write(text)
}

Important note about delegated members

If a delegated object calls one of its own methods internally, it does not dispatch to overrides in the delegating class.

interface Service {
    fun operation()
    fun run()
}

class DefaultService : Service {
    override fun operation() {
        println("Default operation")
    }

    override fun run() {
        operation()
    }
}

class CustomService(
    private val service: Service
) : Service by service {
    override fun operation() {
        println("Custom operation")
    }
}

Usage:

fun main() {
    val service = CustomService(DefaultService())
    service.operation()
    service.run()
}

Output:

Custom operation
Default operation

service.run() is delegated to DefaultService.run(), and inside that object, operation() resolves to DefaultService.operation().


Property delegation

Property delegation lets another object provide the getter and/or setter for a property.

The syntax is:

val propertyName: Type by delegate
var propertyName: Type by delegate

The delegate object must provide:

  • getValue(...) for val
  • getValue(...) and setValue(...) for var

1. Built-in property delegates

Kotlin provides several common delegates.


lazy

lazy initializes a value only when it is first accessed.

val expensiveValue: String by lazy {
    println("Computing value")
    "Hello"
}

fun main() {
    println("Before access")
    println(expensiveValue)
    println(expensiveValue)
}

Output:

Before access
Computing value
Hello
Hello

The initializer runs only once.


observable

Delegates.observable runs code whenever a property changes.

import kotlin.properties.Delegates

var name: String by Delegates.observable("Unknown") { property, oldValue, newValue ->
    println("${property.name} changed from $oldValue to $newValue")
}

fun main() {
    name = "Alice"
    name = "Bob"
}

Output:

name changed from Unknown to Alice
name changed from Alice to Bob

vetoable

Delegates.vetoable can reject a new value.

import kotlin.properties.Delegates

var age: Int by Delegates.vetoable(0) { _, _, newValue ->
    newValue >= 0
}

fun main() {
    age = 25
    println(age)

    age = -5
    println(age)
}

Output:

25
25

The assignment to -5 is rejected.


notNull

Delegates.notNull() is useful for non-null properties initialized later.

import kotlin.properties.Delegates

var username: String by Delegates.notNull()

fun main() {
    username = "alice"
    println(username)
}

If you read username before assigning it, Kotlin throws an exception.


Custom property delegate

You can create your own delegate by implementing getValue and optionally setValue.

import kotlin.reflect.KProperty

class LoggingDelegate {
    private var value: String = ""

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("Reading ${property.name}")
        return value
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
        println("Writing ${property.name}: $newValue")
        value = newValue
    }
}

class User {
    var name: String by LoggingDelegate()
}

Usage:

fun main() {
    val user = User()

    user.name = "Alice"
    println(user.name)
}

Output:

Writing name: Alice
Reading name
Alice

How getValue and setValue work

For:

var name: String by LoggingDelegate()

Kotlin roughly translates property access like this:

val delegate = LoggingDelegate()

delegate.setValue(thisRef, property, "Alice")
val value = delegate.getValue(thisRef, property)

The signatures are:

operator fun getValue(thisRef: Any?, property: KProperty<*>): T
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T)

For top-level properties, thisRef is null.


Delegating to another property

You can also delegate one property to another property reference.

class User {
    var name: String = "Alice"

    var displayName: String by this::name
}

fun main() {
    val user = User()

    println(user.displayName)

    user.displayName = "Bob"

    println(user.name)
}

Output:

Alice
Bob

displayName delegates its storage to name.


Delegating to a map

A common pattern is delegating properties to a Map.

class User(
    private val data: Map<String, Any?>
) {
    val name: String by data
    val age: Int by data
}

Usage:

fun main() {
    val user = User(
        mapOf(
            "name" to "Alice",
            "age" to 30
        )
    )

    println(user.name)
    println(user.age)
}

Output:

Alice
30

For mutable properties, use MutableMap:

class MutableUser(
    private val data: MutableMap<String, Any?>
) {
    var name: String by data
    var age: Int by data
}

Summary

Use case Syntax Meaning
Interface delegation class C(d: I) : I by d Forward interface calls to d
Lazy property val x by lazy { ... } Compute once on first access
Observable property var x by Delegates.observable(...) React to changes
Vetoable property var x by Delegates.vetoable(...) Accept or reject changes
Custom delegate var x by MyDelegate() Delegate getter/setter logic
Map-backed property val x: T by map Read value from map by property name
Property reference var x by this::other Delegate to another property

In short:

class MyClass(delegate: SomeInterface) : SomeInterface by delegate

is for interface delegation, while:

val value by lazy { ... }
var name by MyDelegate()

is for property delegation.