AlgoGalaxy

AlgoGalaxy

University
CodeRush
Jobs
Tools
Insights
Explore
Personalize
JavaGoPHPRustGraphsTreesArraysDynamic ProgrammingGreedyHashingHeapLinked ListNumber TheoryStackStringsTrieUnion FindBacktrackingBinary SearchBit Manipulation

Kotlin Sealed Classes and Visibility

A sealed hierarchy models a finite set of states. Kotlin can require an exhaustive when expression, so new states become visible wherever a result must be handled. Pair that with careful visibility: expose the public outcome callers need, and keep mutable implementation details behind the class that owns them.

Work through the programs in order. Each example changes one idea at a time, includes the expected result, and asks you to make a small change. That practice turns object-oriented vocabulary into code you can confidently write, review, and test.

What You Will Build

This is a code lab, not a compressed definition. The five examples begin with the smallest working form, then add one responsibility, one rule, or one alternative path. Keep a Kotlin compiler open while reading and run every block before moving to the next one.

In production code, models should make ownership, mutation, and failure visible. Give a class one reason to change, validate at its boundary, and return a named result when an operation can be rejected. Kotlin syntax supports that clarity, but it cannot make an unstated business rule safe.

Example 1: Model finite outcomes

A sealed interface describes every supported result. The compiler knows this family is closed in the module, so a when expression can handle each known case.

Kotlin

sealed interface LoadResult {
    data class Success(val title: String) : LoadResult
    data class Failure(val reason: String) : LoadResult
}

fun main() {
    val result: LoadResult = LoadResult.Success("Kotlin")
    println(result)
}

Expected Result

Success(title=Kotlin)

Try next: Create a Failure value and compare its explicit state with using null for every possible problem.

Trace the program from construction through its printed result. Change one input before you continue; a reliable object model should make the new outcome predictable without hidden mutable state or a surprise fallback.

Example 2: Handle every state with when

An exhaustive when turns each result into a caller-visible message. There is no generic else branch to quietly hide a newly added state.

Kotlin

sealed interface LoadResult {
    data class Success(val title: String) : LoadResult
    data class Failure(val reason: String) : LoadResult
}

fun label(result: LoadResult): String = when (result) {
    is LoadResult.Success -> "Loaded ${result.title}"
    is LoadResult.Failure -> "Failed: ${result.reason}"
}

fun main() {
    println(label(LoadResult.Failure("offline")))
}

Expected Result

Failed: offline

Try next: Add a Loading state and let the compiler show every place that now needs a deliberate response.

Trace the program from construction through its printed result. Change one input before you continue; a reliable object model should make the new outcome predictable without hidden mutable state or a surprise fallback.

Example 3: Use a singleton state when it has no data

object creates one named value. Use it for a true singleton state such as loading rather than allocating an empty data class repeatedly, then render it through an explicit branch instead of relying on its generated identity text.

Kotlin

sealed interface ScreenState {
    object Loading : ScreenState
    data class Content(val count: Int) : ScreenState
}

fun label(state: ScreenState): String = when (state) {
    ScreenState.Loading -> "loading"
    is ScreenState.Content -> "items: ${state.count}"
}

fun main() {
    val state: ScreenState = ScreenState.Loading
    println(label(state))
}

Expected Result

loading

Try next: Create Content(3) and observe that the same explicit branch turns its state into a predictable label.

Trace the program from construction through its printed result. Change one input before you continue; a reliable object model should make the new outcome predictable without hidden mutable state or a surprise fallback.

Example 4: Keep mutation private to the owner

A public getter with a private setter gives callers a safe observation point. Only named methods on the class can transition the state.

Kotlin

class Download {
    var progress = 0
        private set

    fun advance() {
        progress = (progress + 25).coerceAtMost(100)
    }
}

fun main() {
    val download = Download()
    download.advance()
    println(download.progress)
}

Expected Result

25

Try next: Call advance five times and confirm the invariant keeps progress at 100 rather than allowing an impossible value.

Trace the program from construction through its printed result. Change one input before you continue; a reliable object model should make the new outcome predictable without hidden mutable state or a surprise fallback.

Example 5: Return a sealed result from a transition

A method can make success and rejection explicit. Callers can render, retry, or report the returned state without reading a hidden mutable flag.

Kotlin

sealed interface TransferResult {
    data class Accepted(val amount: Int) : TransferResult
    data class Rejected(val reason: String) : TransferResult
}

fun transfer(amount: Int): TransferResult = when {
    amount <= 0 -> TransferResult.Rejected("amount must be positive")
    else -> TransferResult.Accepted(amount)
}

fun main() {
    println(transfer(50))
}

Expected Result

Accepted(amount=50)

Try next: Pass 0 and handle Rejected with a when expression instead of assuming every transfer succeeds.

Trace the program from construction through its printed result. Change one input before you continue; a reliable object model should make the new outcome predictable without hidden mutable state or a surprise fallback.

Example 6: Make the input visible

Start by naming the value the next line depends on. This keeps the lesson input explicit before we apply a transformation or call an external boundary. We are applying that habit while revisiting Kotlin Sealed Classes and Visibility.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Sealed Classes and Visibility"))
}

Expected Result

KOTLIN SEALED CLASSES AND VISIBILITY

Try next: Replace the sample value with one from your own notes, predict the result, and then run it.

Example 7: Guard an empty boundary

A small guard makes the allowed and rejected path visible. We want a caller to receive an intentional result instead of discovering an unexplained failure later. We are applying that habit while revisiting Kotlin Sealed Classes and Visibility.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Sealed Classes and Visibility"))
}

Expected Result

KOTLIN SEALED CLASSES AND VISIBILITY

Try next: Try an empty value as well as the normal value and describe why the two outcomes differ.

Example 8: Keep the transformation pure

This step returns a value instead of mixing the rule with display or storage work. That separation gives us a small unit we can run, test, and reuse. We are applying that habit while revisiting Kotlin Sealed Classes and Visibility.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Sealed Classes and Visibility"))
}

Expected Result

KOTLIN SEALED CLASSES AND VISIBILITY

Try next: Call the same rule twice with different inputs and confirm that each result depends only on its input.

Example 9: Make a branch deliberate

Use a named branch where the business rule changes. A future reader should be able to point at the condition and state the result for both sides. We are applying that habit while revisiting Kotlin Sealed Classes and Visibility.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Sealed Classes and Visibility"))
}

Expected Result

KOTLIN SEALED CLASSES AND VISIBILITY

Try next: Change the boundary value by one and explain exactly which branch becomes active.

Example 10: Use a collection without hidden mutation

Collections become safer when we can see which value is read, which value is derived, and whether the original collection remains owned by the caller. We are applying that habit while revisiting Kotlin Sealed Classes and Visibility.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Sealed Classes and Visibility"))
}

Expected Result

KOTLIN SEALED CLASSES AND VISIBILITY

Try next: Add one more item and predict the derived result before executing the code.

Example 11: Return a named outcome

A named outcome is easier to test than an implied side effect. We can now distinguish a successful path from a rejected path without guessing from console noise. We are applying that habit while revisiting Kotlin Sealed Classes and Visibility.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Sealed Classes and Visibility"))
}

Expected Result

KOTLIN SEALED CLASSES AND VISIBILITY

Try next: Rename the outcome in your own words while preserving the same success and failure rule.

Example 12: Trace a small loop

Work through one value at a time. The point is not to memorize syntax; it is to see how the accumulator or output changes after each repeatable step. We are applying that habit while revisiting Kotlin Sealed Classes and Visibility.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Sealed Classes and Visibility"))
}

Expected Result

KOTLIN SEALED CLASSES AND VISIBILITY

Try next: Use a shorter collection first, then add one value and trace the extra iteration aloud.

Example 13: Keep display at the edge

The core rule prepares a value, while the outer line decides how to show it. That keeps the result usable by a UI, test, API, or command-line caller. We are applying that habit while revisiting Kotlin Sealed Classes and Visibility.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Sealed Classes and Visibility"))
}

Expected Result

KOTLIN SEALED CLASSES AND VISIBILITY

Try next: Change only the display text and verify that the underlying rule and result stay unchanged.

Example 14: Test the closest boundary

A useful example includes the nearest boundary, not only the happy path. We are making the behavior inspectable before a real caller relies on it. We are applying that habit while revisiting Kotlin Sealed Classes and Visibility.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Sealed Classes and Visibility"))
}

Expected Result

KOTLIN SEALED CLASSES AND VISIBILITY

Try next: Try the lowest supported value and then one value just outside the accepted range.

Example 15: Write a tiny regression check

A compact assertion turns the lesson into a repeatable promise. It helps us catch a changed rule before a larger program hides the source of the mistake. We are applying that habit while revisiting Kotlin Sealed Classes and Visibility.

Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.

Kotlin

fun review(value: String?): String =
    value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
        ?: "needs a value"

fun main() {
    println(review("Kotlin Sealed Classes and Visibility"))
}

Expected Result

KOTLIN SEALED CLASSES AND VISIBILITY

Try next: Deliberately change the expected value once, observe the failed check, and then restore the correct contract.

Practice Checklist

Reproduce all five programs from memory before moving forward. Then make one small original model with a meaningful constructor, a named operation, and an explicit result for invalid input or an alternate state.

  • Practice 1: recreate “Model finite outcomes” without copying, change one value, and explain the result before running it.
  • Practice 2: recreate “Handle every state with when” without copying, change one value, and explain the result before running it.
  • Practice 3: recreate “Use a singleton state when it has no data” without copying, change one value, and explain the result before running it.
  • Practice 4: recreate “Keep mutation private to the owner” without copying, change one value, and explain the result before running it.
  • Practice 5: recreate “Return a sealed result from a transition” without copying, change one value, and explain the result before running it.

If you cannot explain who owns a mutable value, which inputs are valid, or what the caller sees on failure, the design needs one more clear boundary before it grows.

Common Mistakes

  • Making every property public and mutable before defining the class operation that should own the change.
  • Using inheritance just to reuse a few lines when a focused collaborator would make the relationship clearer.
  • Returning null or an unlabelled Boolean when a caller needs an explicit success, rejection, loading, or failure state.
  • Copying a working program without changing an input and therefore never testing whether the rule was understood.
logo of AlgoGalaxy

Newsletter

Subscribe to our newsletter for weekly updates and promotions.

AlgoGalaxy University

University HomeTechnology LearningCoding ProblemsLearning ProgressCareer OpportunitiesTechnology InsightsUtilNexa ToolsAlgoGalaxy HomeContact Support

AlgoGalaxy CodeRush

CodeRush HomeProblem SetsProgress TrackerLearn TechnologyTechnology JobsEngineering InsightsDeveloper UtilitiesAlgoGalaxy HomePrivacy Policy
Privacy Policy

 • 

Terms of Service

Copyright © AlgoGalaxy 2026