RizTech Academy logo
RizTech Academy
Design Patterns, the Kotlin WayLesson 4 of 535 min

Strategy, observer and state with lambdas, Flow and sealed classes

Behavioural patterns are about how objects interact and share behaviour — and this is where Kotlin's first-class functions dissolve the classic catalogue most dramatically. Strategy, Observer, and State are three of the most-taught patterns; in Kotlin they become a lambda, a Flow, and a sealed class. Learn the problem each solves, then see how the language answers it directly.

Strategy — swappable behaviour

The problem: you want to vary a piece of behaviour — a sorting rule, a pricing policy, a validation — without rewriting the code around it. The Strategy pattern defines a family of interchangeable behaviours and lets you pick one at runtime.

The Java pattern: an interface with one method, a class per strategy, and code to select and pass them. In Kotlin, a strategy is a lambda — because functions are values (the functions module):

// Java would need: interface DiscountStrategy { fun apply(p: Int): Int } + a class each.
// Kotlin: the strategy IS a function type.
val noDiscount: (Int) -> Int = { it }
val tenPercent: (Int) -> Int = { it - it / 10 }
val flat50:     (Int) -> Int = { it - 5000 }

fun priceAfter(basePaise: Int, discount: (Int) -> Int): Int = discount(basePaise)

println(priceAfter(100000, tenPercent))    // 90000
println(priceAfter(100000, flat50))        // 95000
println(priceAfter(100000) { it })         // 100000 — a strategy inline as a trailing lambda

The whole Strategy pattern — a family of interchangeable behaviours, selected and passed at runtime — is just a function type (Int) -> Int and some lambdas. No interface, no class per strategy, no wiring. This is the single clearest example of a pattern becoming a language feature: Strategy in Kotlin is "pass a lambda", and you have been doing it since the functions module every time you called list.sortedBy { } (the sort strategy is the lambda) or filter { } (the predicate strategy is the lambda).

Observer — notify many when something changes

The problem: when one thing changes, several other parts of the program need to know — a data model changes and three screens must update. The Observer pattern lets observers "subscribe" to a subject and be notified on change.

The simple Kotlin form is a list of callback lambdas:

class Cart {
    private val listeners = mutableListOf<(Int) -> Unit>()
    var itemCount = 0
        private set

    fun onChange(listener: (Int) -> Unit) { listeners.add(listener) }

    fun add() {
        itemCount++
        listeners.forEach { it(itemCount) }     // notify every observer
    }
}

val cart = Cart()
cart.onChange { count -> println("Badge shows $count") }
cart.onChange { count -> println("Checkout button ${if (count > 0) "enabled" else "disabled"}") }
cart.add()      // both observers run: "Badge shows 1", "Checkout button enabled"

Again, the observers are just lambdas in a list — no Observer interface, no Observable base class. But the modern Kotlin answer to Observer, and the one you will actually use on Android, is Flow (the coroutines module): a stream of values that observers collect. Flow and StateFlow are the Observer pattern, industrialised — the subject emits values, and any number of collectors react, with lifecycle-safe subscription and all the transformation operators. When you expose a StateFlow from a ViewModel and the UI collects it, you are using Observer without ever writing the pattern by hand. Observer is not gone in Kotlin; it is built into Flow.

State — behaviour that depends on which state you are in

The problem: an object behaves differently depending on its current state — a media player responds to "play" differently when stopped, playing, or paused. The State pattern models each state and the transitions between them.

The Kotlin answer is a sealed class and an exhaustive when (the classes module):

sealed class PlayerState {
    object Stopped : PlayerState()
    data class Playing(val track: String) : PlayerState()
    data class Paused(val track: String, val position: Int) : PlayerState()
}

fun onPlay(state: PlayerState): PlayerState = when (state) {
    is PlayerState.Stopped -> PlayerState.Playing("first track")
    is PlayerState.Playing -> state                              // already playing
    is PlayerState.Paused  -> PlayerState.Playing(state.track)   // resume
    // no else — the compiler checks every state is handled
}

The sealed class enumerates the states (each carrying exactly the data that state needs), and the when handles every one — with the compiler guaranteeing exhaustiveness, so adding a new state breaks the when until you handle it. This is more expressive and safer than the Java State pattern's class-per-state hierarchy, because the compiler enforces that every state is accounted for. It is the standard way to model UI state on Android, and you saw it in the sealed-classes lesson for exactly this reason.

The scoreboard

Pattern The problem Kotlin's answer
Strategy Swappable behaviour A lambda / function type — you already use it in sortedBy, filter
Observer Notify many on change Callbacks, and industrial-strength Flow/StateFlow
State Behaviour per state A sealed class + exhaustive when
Command Encapsulate an action A lambda (a () -> Unit)
Template Method Fixed skeleton, varying steps A function taking lambdas for the varying steps

Look at how many rows say "lambda". That is the deep point of the whole module: first-class functions collapse most behavioural patterns, because those patterns were largely ways to pass behaviour around in languages that could not pass functions. Kotlin can, so you pass a function.

Check your work

What Strategy solves, and Kotlin's answer. Swappable behaviour; a lambda / function type — no interface or class per strategy.

Where you already use Strategy. Every sortedBy { }, filter { }, maxByOrNull { } — the lambda is the strategy.

What Observer solves. Notifying many parts of the program when one thing changes.

The modern Kotlin form of Observer. Flow / StateFlow — the subject emits, collectors react, lifecycle-safe.

What State solves, and Kotlin's answer. Behaviour that depends on the current state; a sealed class plus an exhaustive when.

Why the sealed-class State is safer than the Java form. The compiler guarantees every state is handled, and breaks the when when you add one.

Why so many behavioural patterns collapse in Kotlin. They were ways to pass behaviour around; first-class functions let you just pass a lambda.

Command and Template Method in Kotlin. A lambda (() -> Unit), and a function taking lambdas for the varying steps.

Practice

  1. Write three discount strategies as (Int) -> Int lambdas and a priceAfter(base, discount) function. Apply each, and one more inline as a trailing lambda.
  2. Recognise the Strategy pattern in list.sortedBy { it.length } — explain what the "strategy" is.
  3. Write a Cart with an observer list of lambdas, register two observers, and confirm both run on change.
  4. Explain, in two sentences, how a StateFlow collected by the UI is the Observer pattern.
  5. Model a TrafficLight or PlayerState as a sealed class and write an exhaustive when for a transition. Add a new state and watch the when break.
  6. Write a Command as a () -> Unit lambda stored in a list, and "execute" them in order (an undo stack is the classic use).
  7. Take any behavioural pattern you know from Java and write its Kotlin form. Count the lines saved.

Official documentation

Next: when a pattern is the wrong answer.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship