How do I use sealed classes for exhaustive type-safe hierarchies in Kotlin?

In Kotlin, sealed classes (and sealed interfaces) let you model a closed, type-safe hierarchy: a fixed set of known subtypes. This is especially useful for things like UI state, results, commands, events, and domain-specific alternatives.

Basic idea

A sealed class restricts which classes can inherit from it.

sealed class Result

data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
data object Loading : Result()

Now Result can only be one of the known subclasses: Success, Error, or Loading.

Exhaustive when

The main benefit is that Kotlin can check whether a when expression handles every possible subtype.

fun render(result: Result): String {
    return when (result) {
        is Success -> "Data: ${result.data}"
        is Error -> "Error: ${result.message}"
        Loading -> "Loading..."
    }
}

Because Result is sealed, the compiler knows all possible cases. You do not need an else branch if all cases are covered.

If you add another subtype:

data object Empty : Result()

Then this when becomes incomplete, and the compiler will require you to handle Empty.

Prefer data object for singleton states

For sealed hierarchies with singleton cases, use data object:

sealed class UiState {
    data object Loading : UiState()
    data object Empty : UiState()
    data class Success(val items: List<String>) : UiState()
    data class Error(val cause: Throwable) : UiState()
}

Usage:

fun message(state: UiState): String =
    when (state) {
        UiState.Loading -> "Loading"
        UiState.Empty -> "No items"
        is UiState.Success -> "Loaded ${state.items.size} items"
        is UiState.Error -> "Failed: ${state.cause.message}"
    }

Sealed classes vs enums

Use an enum class when every case is a simple constant:

enum class Direction {
    NORTH, SOUTH, EAST, WEST
}

Use a sealed class when cases may carry different data:

sealed class PaymentStatus {
    data object Pending : PaymentStatus()
    data class Paid(val receiptId: String) : PaymentStatus()
    data class Failed(val reason: String) : PaymentStatus()
}

Sealed interfaces

A sealed interface is useful when subclasses may also extend another class, or when you want multiple sealed abstractions.

sealed interface NetworkState

data object Offline : NetworkState
data object Connecting : NetworkState
data class Online(val bandwidthMbps: Int) : NetworkState

Usage:

fun describe(state: NetworkState): String =
    when (state) {
        Offline -> "Offline"
        Connecting -> "Connecting"
        is Online -> "Online at ${state.bandwidthMbps} Mbps"
    }

Nesting subclasses inside the sealed type

A common style is to define all cases inside the sealed class for readability:

sealed class AuthResult {
    data class Success(val userId: String) : AuthResult()
    data object InvalidCredentials : AuthResult()
    data object NetworkFailure : AuthResult()
}

Then use it like this:

fun handle(result: AuthResult): String =
    when (result) {
        is AuthResult.Success -> "Welcome ${result.userId}"
        AuthResult.InvalidCredentials -> "Invalid username or password"
        AuthResult.NetworkFailure -> "Please check your connection"
    }

Generic sealed result type

A common pattern is a generic result wrapper:

sealed class AppResult<out T> {
    data class Success<T>(val value: T) : AppResult<T>()
    data class Failure(val error: Throwable) : AppResult<Nothing>()
    data object Loading : AppResult<Nothing>()
}

Example usage:

fun display(result: AppResult<String>): String =
    when (result) {
        is AppResult.Success -> "Value: ${result.value}"
        is AppResult.Failure -> "Error: ${result.error.message}"
        AppResult.Loading -> "Loading..."
    }

The out T makes AppResult covariant, so AppResult<String> can be used where AppResult<Any> is expected.

Rules to remember

In modern Kotlin:

  • Direct subclasses of a sealed class/interface must be in the same package.
  • They must be declared in the same module.
  • Sealed subclasses can be top-level or nested.
  • A sealed class is abstract by default.
  • Sealed classes cannot be instantiated directly.

Example:

sealed class Command

data class CreateUser(val name: String) : Command()
data class DeleteUser(val id: Long) : Command()
data object Sync : Command()

Exhaustive when as an expression

For exhaustiveness checking, prefer using when as an expression:

val text = when (state) {
    UiState.Loading -> "Loading"
    UiState.Empty -> "Empty"
    is UiState.Success -> "Success"
    is UiState.Error -> "Error"
}

If you use when only as a statement, exhaustiveness checking may be less useful depending on context and Kotlin version/settings.

Practical example: UI state

sealed interface ProfileUiState {
    data object Loading : ProfileUiState
    data class Loaded(
        val name: String,
        val email: String
    ) : ProfileUiState
    data class Failed(val message: String) : ProfileUiState
}

fun renderProfile(state: ProfileUiState): String =
    when (state) {
        ProfileUiState.Loading -> "Loading profile..."
        is ProfileUiState.Loaded -> """
            Name: ${state.name}
            Email: ${state.email}
        """.trimIndent()
        is ProfileUiState.Failed -> "Could not load profile: ${state.message}"
    }

Best practices

  • Use sealed types to represent a closed set of alternatives.
  • Use data class for cases with data.
  • Use data object for singleton cases.
  • Avoid unnecessary else branches in when; let the compiler check exhaustiveness.
  • Keep sealed hierarchies small and meaningful.
  • Prefer sealed interfaces when you need more flexible inheritance.
  • Prefer enums for simple constant-only sets.

In short: sealed classes give you algebraic data type-style modeling in Kotlin, with compiler-checked exhaustive handling through when.

How do I use enums and associate data or behavior in Kotlin?

In Kotlin, enum class is used for a fixed set of constants. Enums can have:

  • simple constants
  • constructor parameters / associated data
  • properties
  • functions
  • overridden behavior per constant
  • companion object utilities
  • implemented interfaces

Basic enum

enum class Direction {
    NORTH,
    SOUTH,
    EAST,
    WEST
}

Usage:

val direction = Direction.NORTH

when (direction) {
    Direction.NORTH -> println("Going up")
    Direction.SOUTH -> println("Going down")
    Direction.EAST -> println("Going right")
    Direction.WEST -> println("Going left")
}

Kotlin when is exhaustive for enums if you cover all constants, so you often do not need an else.


Enum with associated data

Enums can have a constructor.

enum class HttpStatus(val code: Int, val reason: String) {
    OK(200, "OK"),
    CREATED(201, "Created"),
    BAD_REQUEST(400, "Bad Request"),
    NOT_FOUND(404, "Not Found"),
    INTERNAL_SERVER_ERROR(500, "Internal Server Error")
}

Usage:

val status = HttpStatus.NOT_FOUND

println(status.code)   // 404
println(status.reason) // Not Found

Important syntax rule: if an enum has members after the constants, the constant list must end with a semicolon.

enum class HttpStatus(val code: Int) {
    OK(200),
    NOT_FOUND(404);

    fun isSuccess(): Boolean = code in 200..299
}

Enum with shared behavior

You can define functions inside the enum class.

enum class Planet(val mass: Double, val radius: Double) {
    EARTH(5.972e24, 6.371e6),
    MARS(6.39e23, 3.389e6),
    JUPITER(1.898e27, 6.9911e7);

    fun surfaceGravity(): Double {
        val gravitationalConstant = 6.67430e-11
        return gravitationalConstant * mass / (radius * radius)
    }
}

Usage:

println(Planet.EARTH.surfaceGravity())

Enum constants with different behavior

Each enum constant can override functions.

enum class Operation {
    PLUS {
        override fun apply(a: Int, b: Int): Int = a + b
    },
    MINUS {
        override fun apply(a: Int, b: Int): Int = a - b
    },
    TIMES {
        override fun apply(a: Int, b: Int): Int = a * b
    },
    DIVIDE {
        override fun apply(a: Int, b: Int): Int = a / b
    };

    abstract fun apply(a: Int, b: Int): Int
}

Usage:

val result = Operation.TIMES.apply(6, 7)
println(result) // 42

This pattern is useful when the enum represents a strategy or command.


Enum implementing an interface

Enums can implement interfaces.

interface Printable {
    fun label(): String
}

enum class Priority : Printable {
    LOW {
        override fun label(): String = "Low priority"
    },
    MEDIUM {
        override fun label(): String = "Medium priority"
    },
    HIGH {
        override fun label(): String = "High priority"
    }
}

Usage:

val priority: Printable = Priority.HIGH
println(priority.label())

You can also combine constructor data with an interface:

interface HasCode {
    val code: Int
}

enum class ErrorType(
    override val code: Int,
    val message: String
) : HasCode {
    VALIDATION(100, "Validation failed"),
    AUTHENTICATION(200, "Authentication failed"),
    NOT_FOUND(300, "Resource not found")
}

Companion object lookup helpers

A common pattern is looking up enum values by associated data.

enum class HttpStatus(val code: Int) {
    OK(200),
    CREATED(201),
    BAD_REQUEST(400),
    NOT_FOUND(404);

    companion object {
        fun fromCode(code: Int): HttpStatus? {
            return entries.find { it.code == code }
        }
    }
}

Usage:

val status = HttpStatus.fromCode(404)
println(status) // NOT_FOUND

In modern Kotlin, prefer entries over values():

HttpStatus.entries

instead of:

HttpStatus.values()

Built-in enum properties and functions

Every enum constant has:

val name: String
val ordinal: Int

Example:

enum class Color {
    RED,
    GREEN,
    BLUE
}

println(Color.RED.name)    // RED
println(Color.RED.ordinal) // 0

You can parse by name:

val color = enumValueOf<Color>("RED")
println(color) // RED

Or safely:

val color = Color.entries.find { it.name == "RED" }

Enum with custom display names

Avoid relying on name for user-facing text. Use a property instead.

enum class UserRole(val displayName: String) {
    ADMIN("Administrator"),
    EDITOR("Editor"),
    VIEWER("Viewer")
}

Usage:

println(UserRole.ADMIN.displayName) // Administrator

Enum with properties and computed values

enum class FileType(val extension: String) {
    TEXT("txt"),
    JSON("json"),
    CSV("csv");

    val mimeType: String
        get() = when (this) {
            TEXT -> "text/plain"
            JSON -> "application/json"
            CSV -> "text/csv"
        }
}

Usage:

println(FileType.JSON.extension) // json
println(FileType.JSON.mimeType)  // application/json

When to use enums

Use an enum when:

  • the set of values is fixed
  • each value is a singleton
  • you need exhaustive when handling
  • the values are known at compile time

Good examples:

enum class LogLevel {
    TRACE,
    DEBUG,
    INFO,
    WARN,
    ERROR
}
enum class PaymentStatus {
    PENDING,
    PAID,
    FAILED,
    REFUNDED
}

When not to use enums

If each variant needs different state shapes, consider a sealed class or sealed interface.

For example, this is better as a sealed type:

sealed interface UiState {
    data object Loading : UiState
    data class Success(val data: String) : UiState
    data class Error(val message: String) : UiState
}

Because Success and Error need per-instance data, not fixed singleton enum constants.


Quick summary

enum class Status(val code: Int) {
    ACTIVE(1),
    DISABLED(2),
    DELETED(3);

    fun isVisible(): Boolean = this != DELETED

    companion object {
        fun fromCode(code: Int): Status? =
            entries.find { it.code == code }
    }
}

Usage:

val status = Status.fromCode(1)

if (status?.isVisible() == true) {
    println("Show item")
}

Kotlin enums are best for fixed named values, and they can carry data and behavior just like small classes.

How do I use object expressions and object declarations in Kotlin?

Object expressions vs. object declarations in Kotlin

Kotlin has two closely related features:

  • Object expressions: create an anonymous object immediately.
  • Object declarations: create a named singleton object.

They both use the object keyword, but they are used for different purposes.


1. Object expressions

Use an object expression when you need a one-off object, often to implement an interface or extend a class without creating a named class.

Basic anonymous object

fun main() {
    val user = object {
        val name = "Ava"
        val age = 30

        fun greet() {
            println("Hello, my name is $name")
        }
    }

    println(user.name)
    user.greet()
}

Here, user is an anonymous object with properties and functions.


2. Implementing an interface with an object expression

Object expressions are commonly used for callbacks, listeners, and small implementations.

interface ClickListener {
    fun onClick()
}

fun setClickListener(listener: ClickListener) {
    listener.onClick()
}

fun main() {
    setClickListener(object : ClickListener {
        override fun onClick() {
            println("Button clicked")
        }
    })
}

The syntax is:

object : SomeInterface {
    override fun someFunction() {
        // implementation
    }
}

3. Extending a class with an object expression

You can also create an anonymous subclass.

open class Animal(val name: String) {
    open fun speak() {
        println("$name makes a sound")
    }
}

fun main() {
    val dog = object : Animal("Buddy") {
        override fun speak() {
            println("$name barks")
        }
    }

    dog.speak()
}

4. Implementing multiple types

An object expression can extend one class and implement one or more interfaces.

open class Logger {
    open fun log(message: String) {
        println("Log: $message")
    }
}

interface Closeable {
    fun close()
}

fun main() {
    val resource = object : Logger(), Closeable {
        override fun log(message: String) {
            println("Custom log: $message")
        }

        override fun close() {
            println("Resource closed")
        }
    }

    resource.log("Started")
    resource.close()
}

If a superclass has a constructor, call it after the class name:

object : Logger()

5. Object declarations

Use an object declaration when you want a named singleton: exactly one instance, created lazily when first used.

object DatabaseConfig {
    val url = "jdbc:postgresql://localhost:5432/app"
    val username = "admin"

    fun connect() {
        println("Connecting to $url as $username")
    }
}

fun main() {
    DatabaseConfig.connect()
}

You do not instantiate it with DatabaseConfig().

Use it directly by name:

DatabaseConfig.connect()

6. Object declarations can implement interfaces

interface Analytics {
    fun track(event: String)
}

object ConsoleAnalytics : Analytics {
    override fun track(event: String) {
        println("Tracking event: $event")
    }
}

fun main() {
    ConsoleAnalytics.track("AppOpened")
}

This is useful for global services, registries, configuration, or strategy objects.


7. Object declarations can extend classes

open class AppLogger {
    open fun info(message: String) {
        println("INFO: $message")
    }
}

object Logger : AppLogger() {
    override fun info(message: String) {
        println("[App] $message")
    }
}

fun main() {
    Logger.info("Application started")
}

8. Companion objects

A companion object is an object declaration inside a class. It is commonly used for factory methods and static-like members.

class User private constructor(val name: String) {
    companion object {
        fun create(name: String): User {
            return User(name.trim())
        }
    }
}

fun main() {
    val user = User.create("  Ava  ")
    println(user.name)
}

You call companion object members through the class name:

User.create("Ava")

9. Named companion objects

A companion object can have a name.

class App {
    companion object Config {
        const val VERSION = "1.0.0"

        fun printVersion() {
            println("Version: $VERSION")
        }
    }
}

fun main() {
    App.printVersion()
    App.Config.printVersion()
}

Both calls are valid:

App.printVersion()
App.Config.printVersion()

10. Object expression visibility detail

If an anonymous object is stored in a local variable, you can access its members:

fun main() {
    val person = object {
        val name = "Mira"
        fun sayHi() = println("Hi, I am $name")
    }

    println(person.name)
    person.sayHi()
}

But if an anonymous object is returned from a public function, its specific members are not visible unless the return type exposes them.

class Factory {
    fun createPublic(): Any = object {
        val name = "Hidden"
    }

    private fun createPrivate() = object {
        val name = "Visible inside class"
    }

    fun demo() {
        val item = createPrivate()
        println(item.name)
    }
}

In this example:

fun createPublic(): Any = object {
    val name = "Hidden"
}

The caller only sees Any, not name.


11. Quick comparison

Feature Object expression Object declaration
Purpose One-off anonymous object Named singleton
Has a name? Usually no Yes
Instantiated with constructor? No No
Created when? Immediately where expression runs Lazily on first access
Common use Callbacks, temporary implementations Singletons, global config, shared services
Can extend classes? Yes Yes
Can implement interfaces? Yes Yes

Simple rule of thumb

Use an object expression when you need an object right here, right now:

val listener = object : ClickListener {
    override fun onClick() {
        println("Clicked")
    }
}

Use an object declaration when you need one shared named instance:

object AppSettings {
    val theme = "dark"
}

How do I create and use companion objects for static-like behavior in Kotlin?

In Kotlin, a companion object is an object declared inside a class that can hold members callable on the class itself, giving you static-like behavior.

Kotlin does not have Java-style static members directly. Instead, you usually use companion object.

Basic example

class User(val name: String) {
    companion object {
        const val DEFAULT_NAME = "Guest"

        fun createDefault(): User {
            return User(DEFAULT_NAME)
        }
    }
}

You can access companion object members using the class name:

fun main() {
    println(User.DEFAULT_NAME)

    val user = User.createDefault()
    println(user.name)
}

Output:

Guest
Guest

Why it feels like static

This:

User.createDefault()

is similar to calling a static method in Java:

User.createDefault();

But internally, Kotlin’s companion object is an actual singleton object associated with the class.

Companion object properties

You can put properties inside a companion object:

class Counter {
    companion object {
        var count = 0

        fun increment() {
            count++
        }
    }
}

Usage:

fun main() {
    Counter.increment()
    Counter.increment()

    println(Counter.count)
}

Output:

2

Named companion objects

A companion object can have a name:

class Database {
    companion object Factory {
        fun connect(): Database {
            return Database()
        }
    }
}

You can still access members through the class name:

val db = Database.connect()

Or through the companion object name:

val db = Database.Factory.connect()

Factory method example

A common use case is creating factory methods:

class Person private constructor(
    val name: String,
    val age: Int
) {
    companion object {
        fun of(name: String, age: Int): Person {
            require(age >= 0) { "Age cannot be negative" }
            return Person(name, age)
        }
    }
}

Usage:

fun main() {
    val person = Person.of("Alice", 30)
    println(person.name)
}

Constants in companion objects

For compile-time constants, use const val:

class ApiConfig {
    companion object {
        const val BASE_URL = "https://api.example.com"
        const val TIMEOUT_SECONDS = 30
    }
}

Usage:

println(ApiConfig.BASE_URL)

const val can only be used with primitive types and String.

Java interoperability

From Java, companion object members are normally accessed through Companion:

class MathUtils {
    companion object {
        fun double(x: Int): Int = x * 2
    }
}

Java usage:

int result = MathUtils.Companion.double(5);

If you want Java callers to use it like a real static method, add @JvmStatic:

class MathUtils {
    companion object {
        @JvmStatic
        fun double(x: Int): Int = x * 2
    }
}

Then Java can call:

int result = MathUtils.double(5);

For constants:

class Constants {
    companion object {
        const val APP_NAME = "MyApp"
    }
}

Java can access this as:

String appName = Constants.APP_NAME;

Companion objects can implement interfaces

Because companion objects are real objects, they can implement interfaces:

interface Parser<T> {
    fun parse(value: String): T
}

class User(val name: String) {
    companion object : Parser<User> {
        override fun parse(value: String): User {
            return User(value)
        }
    }
}

Usage:

fun main() {
    val user = User.parse("Alice")
    println(user.name)
}

You can also pass the companion object where the interface is expected:

fun <T> parseWith(parser: Parser<T>, value: String): T {
    return parser.parse(value)
}

val user = parseWith(User, "Bob")

Here, User refers to the companion object when used as a value.

Key points

  • Use companion object for static-like members.
  • Access members as ClassName.member.
  • Use const val for compile-time constants.
  • Use @JvmStatic if Java callers need static-style access.
  • Companion objects are real singleton objects.
  • Companion objects can have names and implement interfaces.

How do I use inner and nested classes in Kotlin?

In Kotlin, classes can be declared inside other classes in two main ways:

  1. Nested classes — default behavior
  2. Inner classes — declared with the inner keyword

Nested classes

A class declared inside another class is nested by default.

class Outer {
    class Nested {
        fun message(): String {
            return "Hello from Nested"
        }
    }
}

You create an instance of the nested class using the outer class name:

fun main() {
    val nested = Outer.Nested()
    println(nested.message())
}

Important point

A nested class does not have access to members of the outer class.

class Outer {
    private val name = "Outer"

    class Nested {
        fun printName() {
            // println(name) // Error: cannot access outer class member
        }
    }
}

This is similar to a static nested class in Java.


Inner classes

If you want the nested class to access members of the outer class, mark it with inner.

class Outer {
    private val name = "Outer"

    inner class Inner {
        fun message(): String {
            return "Hello from $name"
        }
    }
}

You create an inner class instance from an instance of the outer class:

fun main() {
    val outer = Outer()
    val inner = outer.Inner()

    println(inner.message())
}

Output:

Hello from Outer

Difference between nested and inner classes

Feature Nested class Inner class
Keyword No keyword needed Uses inner
Has reference to outer class No Yes
Can access outer members No Yes
Instantiation Outer.Nested() Outer().Inner()
Similar to Java static nested class non-static inner class

Accessing this from an inner class

Inside an inner class, this refers to the inner class instance.

To refer to the outer class instance, use this@Outer.

class Outer {
    private val value = "Outer value"

    inner class Inner {
        private val value = "Inner value"

        fun printValues() {
            println(value)
            println(this.value)
            println([email protected])
        }
    }
}

Output:

Inner value
Inner value
Outer value

Example with state

class ShoppingCart {
    private val items = mutableListOf<String>()

    fun addItem(item: String) {
        items.add(item)
    }

    inner class Summary {
        fun printSummary() {
            println("Cart has ${items.size} items")
            println(items.joinToString())
        }
    }
}

fun main() {
    val cart = ShoppingCart()
    cart.addItem("Book")
    cart.addItem("Pen")

    val summary = cart.Summary()
    summary.printSummary()
}

Output:

Cart has 2 items
Book, Pen

Here, Summary is an inner class because it needs access to items from ShoppingCart.


When to use each

Use a nested class when:

  • The class is logically grouped inside another class
  • It does not need access to the outer class instance
  • You want a namespace-like structure
class ApiResponse {
    class Error(val code: Int, val message: String)
}

Use an inner class when:

  • The class needs access to the outer class’s properties or functions
  • Each inner class instance is tied to a specific outer class instance
class Form {
    private val fields = mutableListOf<String>()

    inner class Validator {
        fun validate(): Boolean {
            return fields.isNotEmpty()
        }
    }
}

Summary

class Outer {
    class Nested {
        // No access to Outer instance
    }

    inner class Inner {
        // Has access to Outer instance
    }
}

Use nested classes by default, and use inner only when the inner class needs to access the outer class instance.

How do I use abstract classes and methods in Kotlin?

In Kotlin, an abstract class is a class that cannot be instantiated directly. It is meant to be subclassed.

An abstract method is a method declared without an implementation. Subclasses must override it.

Basic example

abstract class Animal {
    abstract fun makeSound()

    fun sleep() {
        println("Sleeping...")
    }
}

class Dog : Animal() {
    override fun makeSound() {
        println("Woof!")
    }
}

fun main() {
    val dog = Dog()
    dog.makeSound()
    dog.sleep()
}

Output:

Woof!
Sleeping...

Key points

1. Use abstract before the class

abstract class Shape

You cannot create an instance of it:

val shape = Shape() // Error

2. Abstract methods have no body

abstract fun area(): Double

A subclass must implement them:

class Circle(val radius: Double) : Shape() {
    override fun area(): Double {
        return Math.PI * radius * radius
    }
}

3. Abstract classes can have regular methods

abstract class Shape {
    abstract fun area(): Double

    fun describe() {
        println("This is a shape")
    }
}

4. Abstract properties are allowed

abstract class Vehicle {
    abstract val maxSpeed: Int
}

class Car : Vehicle() {
    override val maxSpeed: Int = 200
}

5. Abstract classes can have constructors

abstract class Person(val name: String) {
    abstract fun work()
}

class Developer(name: String) : Person(name) {
    override fun work() {
        println("$name writes code")
    }
}

Complete example

abstract class Shape(val name: String) {
    abstract fun area(): Double

    fun printInfo() {
        println("$name has area ${area()}")
    }
}

class Rectangle(
    name: String,
    val width: Double,
    val height: Double
) : Shape(name) {
    override fun area(): Double {
        return width * height
    }
}

class Circle(
    name: String,
    val radius: Double
) : Shape(name) {
    override fun area(): Double {
        return Math.PI * radius * radius
    }
}

fun main() {
    val shapes = listOf(
        Rectangle("Rectangle", 5.0, 3.0),
        Circle("Circle", 2.0)
    )

    for (shape in shapes) {
        shape.printInfo()
    }
}

Abstract class vs interface

Use an abstract class when you want to share state or constructor logic:

abstract class BaseRepository(val tableName: String) {
    abstract fun findAll(): List<String>
}

Use an interface when you mainly want to define behavior:

interface Drawable {
    fun draw()
}

A class can extend only one abstract class, but it can implement multiple interfaces:

abstract class Animal

interface Runnable {
    fun run()
}

interface Swimmable {
    fun swim()
}

class Duck : Animal(), Runnable, Swimmable {
    override fun run() {
        println("Duck runs")
    }

    override fun swim() {
        println("Duck swims")
    }
}

In short: use abstract class for a shared base with common implementation/state, and use abstract fun or abstract val for members subclasses must provide.

How do I use visibility modifiers (private, internal, protected, public) in Kotlin classes?

In Kotlin, visibility modifiers control where classes, properties, functions, and constructors can be accessed from.

The main visibility modifiers are:

  • public
  • private
  • protected
  • internal

If you do not specify a visibility modifier, Kotlin uses public by default.


1. public

public means the declaration can be accessed from anywhere.

class User {
    public val name: String = "Alice"

    public fun sayHello() {
        println("Hello, $name")
    }
}

Because public is the default, this is equivalent:

class User {
    val name: String = "Alice"

    fun sayHello() {
        println("Hello, $name")
    }
}

Use public when you want something to be part of the normal external API of a class.


2. private

Inside a class, private means the member can only be accessed inside that same class.

class BankAccount {
    private var balance: Double = 0.0

    fun deposit(amount: Double) {
        if (amount > 0) {
            balance += amount
        }
    }

    fun getBalance(): Double {
        return balance
    }
}

Usage:

fun main() {
    val account = BankAccount()

    account.deposit(100.0)

    println(account.getBalance())

    // Error:
    // println(account.balance)
}

Here, balance is hidden from outside code. The class controls access through deposit() and getBalance().

Private class members

class UserService {
    private fun validateName(name: String): Boolean {
        return name.isNotBlank()
    }

    fun createUser(name: String) {
        if (validateName(name)) {
            println("User created: $name")
        }
    }
}

validateName() is an implementation detail and cannot be called from outside UserService.


3. protected

protected means the member is visible inside:

  1. The class where it is declared
  2. Its subclasses

It is not visible to general outside code.

open class Animal {
    protected val name: String = "Animal"

    protected fun makeSound() {
        println("Some sound")
    }
}

class Dog : Animal() {
    fun bark() {
        println(name)
        makeSound()
        println("Woof!")
    }
}

Usage:

fun main() {
    val dog = Dog()

    dog.bark()

    // Error:
    // println(dog.name)

    // Error:
    // dog.makeSound()
}

Dog can access name and makeSound() because it inherits from Animal, but outside code cannot.

Important note about protected

In Kotlin, protected is mainly for class inheritance. Unlike Java, Kotlin does not make protected members visible to the whole package.


4. internal

internal means the declaration is visible within the same module.

A module is usually something like:

  • a Gradle source set
  • a Maven project
  • an IntelliJ IDEA module
  • a set of files compiled together
internal class InternalLogger {
    fun log(message: String) {
        println("LOG: $message")
    }
}

Code in the same module can use it:

fun main() {
    val logger = InternalLogger()
    logger.log("Application started")
}

But code from another module cannot access InternalLogger.

Use internal when you want something available across your project/module but not exposed as a public API to other modules.


Visibility in class members

You can apply visibility modifiers to properties and functions:

class Profile {
    public val username: String = "guest"
    private val passwordHash: String = "abc123"
    internal val accountId: Int = 42
    protected val role: String = "user"

    public fun showUsername() {
        println(username)
    }

    private fun checkPassword() {
        println(passwordHash)
    }
}

However, protected only makes sense in classes that may be inherited from:

open class BaseController {
    protected fun authorize() {
        println("Checking permissions")
    }
}

class UserController : BaseController() {
    fun getUser() {
        authorize()
        println("Returning user")
    }
}

Visibility for constructors

You can also control constructor visibility.

Private constructor

Useful when you want to restrict object creation.

class DatabaseConnection private constructor() {
    companion object {
        fun create(): DatabaseConnection {
            return DatabaseConnection()
        }
    }
}

Usage:

fun main() {
    val connection = DatabaseConnection.create()

    // Error:
    // val direct = DatabaseConnection()
}

Visibility for property setters

A common Kotlin pattern is to expose a property for reading but restrict writing.

class Counter {
    var count: Int = 0
        private set

    fun increment() {
        count++
    }
}

Usage:

fun main() {
    val counter = Counter()

    counter.increment()
    println(counter.count)

    // Error:
    // counter.count = 10
}

Here:

  • count can be read from outside the class
  • count can only be changed inside the class

You can also use protected set or internal set:

open class Document {
    var status: String = "draft"
        protected set
}

Subclasses can change status, but outside code cannot.


Top-level visibility

Kotlin also allows functions, properties, and classes outside classes.

private fun helper() {
    println("Only visible in this file")
}

internal fun moduleHelper() {
    println("Visible in the same module")
}

public fun publicApi() {
    println("Visible everywhere")
}

For top-level declarations:

Modifier Meaning
public Visible everywhere
private Visible only in the same file
internal Visible in the same module
protected Not allowed at top level

Quick comparison

Modifier Class member visibility
public Visible everywhere
private Visible only inside the declaring class
protected Visible inside the declaring class and subclasses
internal Visible inside the same module

For top-level declarations:

Modifier Top-level visibility
public Visible everywhere
private Visible only in the same file
internal Visible inside the same module
protected Not allowed

Practical example

open class Account(
    public val owner: String,
    private var balance: Double
) {
    internal val accountNumber: String = "ACC-123"

    protected fun canWithdraw(amount: Double): Boolean {
        return amount <= balance
    }

    fun deposit(amount: Double) {
        if (amount > 0) {
            balance += amount
        }
    }

    fun currentBalance(): Double {
        return balance
    }
}

class SavingsAccount(owner: String, balance: Double) : Account(owner, balance) {
    fun withdraw(amount: Double) {
        if (canWithdraw(amount)) {
            println("Withdrawal allowed")
        } else {
            println("Insufficient funds")
        }
    }
}

Usage:

fun main() {
    val account = SavingsAccount("Alice", 500.0)

    println(account.owner)
    println(account.currentBalance())

    account.deposit(100.0)
    account.withdraw(50.0)

    // Error: private
    // println(account.balance)

    // Error: protected
    // account.canWithdraw(100.0)

    // Accessible only in the same module:
    // println(account.accountNumber)
}

Rule of thumb

Use the most restrictive visibility that still works:

  • Use private for implementation details.
  • Use protected for behavior intended only for subclasses.
  • Use internal for code shared inside a module but hidden from external modules.
  • Use public for APIs that other code is expected to use.

In most Kotlin classes, you will commonly use private for internal state and leave only the necessary functions or properties public.

How do I implement interfaces and use multiple inheritance in Kotlin?

In Kotlin, interfaces define a contract that classes can implement. Kotlin supports multiple inheritance of interfaces, but not multiple inheritance of classes.

1. Defining an interface

An interface can declare:

  • abstract properties
  • abstract functions
  • functions with default implementations
interface Drivable {
    val maxSpeed: Int

    fun drive()

    fun stop() {
        println("Stopping the vehicle")
    }
}

Here:

  • maxSpeed must be implemented by any class using the interface.
  • drive() has no body, so it must be implemented.
  • stop() has a default implementation, so overriding it is optional.

2. Implementing an interface

Use : after the class name to implement an interface.

class Car : Drivable {
    override val maxSpeed: Int = 180

    override fun drive() {
        println("The car is driving at up to $maxSpeed km/h")
    }
}

Usage:

fun main() {
    val car = Car()

    car.drive()
    car.stop()
}

Output:

The car is driving at up to 180 km/h
Stopping the vehicle

3. Implementing multiple interfaces

A class can implement more than one interface by separating them with commas.

interface Flyable {
    fun fly() {
        println("Flying")
    }
}

interface Swimmable {
    fun swim() {
        println("Swimming")
    }
}

class Duck : Flyable, Swimmable

Usage:

fun main() {
    val duck = Duck()

    duck.fly()
    duck.swim()
}

4. Handling conflicting default implementations

If two interfaces provide a function with the same signature, the implementing class must override it.

interface Printer {
    fun print() {
        println("Printing from Printer")
    }
}

interface Scanner {
    fun print() {
        println("Printing from Scanner")
    }
}

class AllInOneMachine : Printer, Scanner {
    override fun print() {
        super<Printer>.print()
        super<Scanner>.print()
        println("Printing from AllInOneMachine")
    }
}

Usage:

fun main() {
    val machine = AllInOneMachine()

    machine.print()
}

Output:

Printing from Printer
Printing from Scanner
Printing from AllInOneMachine

The syntax:

super<Printer>.print()

means “call the print() implementation from the Printer interface.”

5. Interfaces with properties

Interfaces can declare properties, but they do not store state directly like classes do.

interface Identifiable {
    val id: String
}

class User(
    override val id: String,
    val name: String
) : Identifiable

Usage:

fun main() {
    val user = User("u123", "Alice")

    println(user.id)
    println(user.name)
}

6. Kotlin and multiple inheritance

Kotlin allows:

class Duck : Flyable, Swimmable

But Kotlin does not allow multiple class inheritance:

open class Animal
open class Machine

// Not allowed in Kotlin
class RobotDog : Animal(), Machine()

Instead, Kotlin allows:

  • one superclass
  • multiple interfaces
open class Animal {
    fun eat() {
        println("Eating")
    }
}

interface Runnable {
    fun run()
}

interface Trainable {
    fun train()
}

class Dog : Animal(), Runnable, Trainable {
    override fun run() {
        println("Dog is running")
    }

    override fun train() {
        println("Dog is training")
    }
}

Key syntax

class ClassName : InterfaceName
class ClassName : InterfaceOne, InterfaceTwo
class ClassName : SuperClass(), InterfaceOne, InterfaceTwo

Summary

  • Use interface to define behavior.
  • Use : to implement interfaces.
  • Use override to implement interface members.
  • Kotlin supports multiple interface inheritance.
  • Kotlin does not support inheriting from multiple classes.
  • If interfaces have conflicting default methods, override the method and choose which parent implementation to call using super<InterfaceName>.

How do I use class inheritance with open and override in Kotlin?

In Kotlin, classes and members are final by default, so you must explicitly mark them as open if you want them to be inherited or overridden.

Basic class inheritance

open class Animal {
    open fun makeSound() {
        println("Some sound")
    }
}

class Dog : Animal() {
    override fun makeSound() {
        println("Bark")
    }
}

Usage:

fun main() {
    val dog = Dog()
    dog.makeSound()
}

Output:

Bark

Key rules

1. Use open on a class to allow inheritance

open class Animal

Without open, this is not allowed:

class Dog : Animal()

because Animal would be final by default.

2. Use open on functions or properties to allow overriding

open fun makeSound() {
    println("Some sound")
}

If a function is not marked open, subclasses cannot override it.

3. Use override in the subclass

override fun makeSound() {
    println("Bark")
}

Kotlin requires override so it is clear that you are replacing behavior from the parent class.

Inheriting from a class with a constructor

If the parent class has a constructor, the subclass must call it:

open class Animal(val name: String) {
    open fun introduce() {
        println("I am $name")
    }
}

class Dog(name: String) : Animal(name) {
    override fun introduce() {
        println("I am a dog named $name")
    }
}

Usage:

fun main() {
    val dog = Dog("Buddy")
    dog.introduce()
}

Output:

I am a dog named Buddy

Overriding properties

Properties can also be open and override:

open class Animal {
    open val sound: String = "Some sound"
}

class Dog : Animal() {
    override val sound: String = "Bark"
}

Usage:

fun main() {
    val dog = Dog()
    println(dog.sound)
}

Output:

Bark

Calling the parent implementation with super

You can call the superclass version using super:

open class Animal {
    open fun makeSound() {
        println("Some sound")
    }
}

class Dog : Animal() {
    override fun makeSound() {
        super.makeSound()
        println("Bark")
    }
}

Output:

Some sound
Bark

Preventing further overriding

An overridden member is open by default. If you want to prevent subclasses from overriding it again, mark it as final:

open class Animal {
    open fun makeSound() {
        println("Some sound")
    }
}

open class Dog : Animal() {
    final override fun makeSound() {
        println("Bark")
    }
}

Now subclasses of Dog cannot override makeSound().

Summary

open class Parent {
    open fun method() {
        println("Parent method")
    }
}

class Child : Parent() {
    override fun method() {
        println("Child method")
    }
}
  • open class means the class can be inherited.
  • open fun or open val means the member can be overridden.
  • override means the subclass is replacing a parent member.
  • Use super.method() to call the parent version.

How do I use the init block to run setup logic in Kotlin classes?

In Kotlin, an init block runs setup logic when a class instance is created.

It is commonly used to validate constructor arguments, initialize derived properties, or perform other construction-time setup.

class User(val name: String, val age: Int) {

    init {
        require(name.isNotBlank()) {
            "Name must not be blank"
        }

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

        println("Created user: $name")
    }
}

Usage:

val user = User("Alice", 30)

When User("Alice", 30) is called, the init block runs automatically.

Key points

  • init blocks are part of the class initialization process.
  • They run after primary constructor parameters are available.
  • A class can have multiple init blocks.
  • Multiple init blocks run in the order they appear in the class body.
  • They are especially useful with primary constructors.

Example with multiple init blocks:

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

    val area: Int

    init {
        require(width > 0) {
            "Width must be positive"
        }
    }

    init {
        require(height > 0) {
            "Height must be positive"
        }

        area = width * height
    }
}

You can also combine init blocks with property initializers:

class Product(val price: Double) {

    val tax = price * 0.2

    init {
        require(price >= 0) {
            "Price cannot be negative"
        }
    }
}

Initialization happens in the order the declarations appear in the class body, so property initializers and init blocks are executed top to bottom.