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
- Write three discount strategies as
(Int) -> Intlambdas and apriceAfter(base, discount)function. Apply each, and one more inline as a trailing lambda. - Recognise the Strategy pattern in
list.sortedBy { it.length }— explain what the "strategy" is. - Write a
Cartwith an observer list of lambdas, register two observers, and confirm both run on change. - Explain, in two sentences, how a
StateFlowcollected by the UI is the Observer pattern. - Model a
TrafficLightorPlayerStateas a sealed class and write an exhaustivewhenfor a transition. Add a new state and watch thewhenbreak. - Write a Command as a
() -> Unitlambda stored in a list, and "execute" them in order (an undo stack is the classic use). - Take any behavioural pattern you know from Java and write its Kotlin form. Count the lines saved.
Official documentation
- Kotlin — Higher-order functions and lambdas — The foundation of Strategy, Command, and Template Method in Kotlin.
- Kotlin — Asynchronous Flow — The industrial Observer.
- Kotlin — Sealed classes — The State pattern's Kotlin form.
- Refactoring Guru — Behavioral patterns — What each pattern is, to implement the Kotlin way.
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