Software design · For beginners
Design Patterns in Kotlin: 50+ Patterns With Diagrams and Playground Exercises
Design patterns are named solutions to problems every programmer meets. This article follows the catalogue on Wikipedia’s Software design pattern page: 53 patterns in Kotlin across four families, each with a diagram, a short example, the SOLID principles it practises, and code you can run in the Kotlin Playground. The ones you will use most are marked ★ Popular.
- Creational (10): how objects are made. Singleton, Factory, Builder, Dependency injection, Lazy initialization…
- Structural (11): how objects are combined. Adapter, Decorator, Proxy, Facade, Composite, Delegation…
- Behavioral (16): how objects cooperate. Strategy, Observer, Command, State, Iterator, Chain of responsibility…
- Concurrency (16): many things at once. Lock, Thread pool, Scheduler, Reactor, Active object…
- Link: patterns are SOLID in action; each one points to the matching parts of the SOLID article.
- Rule: use a pattern to remove pain, never to look clever.
Contents
1. What is a design pattern?
A design pattern is a named, proven answer to a problem that keeps appearing in code. It is not a library you import. It is a shape you recognize and reuse, and the name is the real gift: “put a proxy in front of it” replaces a ten-minute explanation.
Think of recipes. “Fold the egg whites into the batter” is a technique with a name. Cooks do not reinvent it each time. Patterns are the named techniques of programming.
Where this article comes from. It is inspired mainly by Wikipedia’s Software design pattern page and follows its catalogue of Creational, Structural, Behavioral and Concurrency patterns. Of the 58 patterns listed there, this article covers 53 (about 91%). The names and families are Wikipedia’s; the explanations, Kotlin code, diagrams and exercises are my own. I left out five that do not translate to Kotlin or Android: Twin (a workaround for languages without multiple inheritance), Front controller and Service handler (server-side web and network servers), Compute kernel (GPU programming) and Safe Concurrency with Exclusive Ownership of Objects (a niche research pattern).
Patterns are SOLID in action
My SOLID principles article gives the five rules of good design. Patterns are the ready-made structures that follow those rules. Under each pattern you will find which SOLID letters it demonstrates, with links to those sections, so you can read the two side by side.
What “★ Popular Popular” means
I marked 25 of the 53 patterns as highly popular. This is my judgment, not a measured statistic: these are the ones you will meet almost every week in Android and Kotlin code (and in interviews), such as Singleton, Factory, Builder, Dependency injection, Adapter, Decorator, Proxy, Facade, Strategy, Observer, Command, State, Iterator, Lock and Thread pool. The rest are real and useful, just less frequent.
Every pattern has a diagram and a Kotlin example you can open in the free Kotlin Playground (new tab, no install, no account). 15 patterns come with a full exercise: a starter with gaps, tests that print PASS or FAIL, a hint and a solution. The others are complete runnable programs: press Run, change something, run again.
Catalogue at a glance
Open a family to see its list. Inside each family the highly popular patterns (★) come first, then the rest. Tap a name to jump to it.
Creational10 patterns, 6 popular
| Pattern | In one line | Popular | Practice |
|---|---|---|---|
| Abstract factory | A factory of factories: one interface that creates a whole family of related objects, so you can swap the entire family (light theme widgets, dark theme widgets) at once. | ★ | Run it |
| Builder | Build a complex object step by step, then validate and produce it in one go. | ★ | Exercise |
| Dependency injection | A class does not create what it needs. | ★ | Exercise |
| Factory | One place decides which class to create, so the rest of the code only knows the interface. | ★ | Exercise |
| Lazy initialization | Delay creating something expensive until the first time it is really needed, then remember it. | ★ | Run it |
| Singleton | Exactly one instance of a class, reachable from everywhere. | ★ | Exercise |
| Multiton | Like Singleton, but one instance per key: a map of named, shared instances. | Run it | |
| Object pool | Keep a set of ready-made, reusable objects and lend them out, instead of creating and destroying them again and again. | Run it | |
| Prototype | Create new objects by copying an existing one and changing a few fields, instead of building from scratch. | Run it | |
| Resource acquisition is initialization (RAII) | Tie a resource (file, connection, lock) to the lifetime of an object, so it is released automatically even when something fails. | Run it |
Structural11 patterns, 7 popular
| Pattern | In one line | Popular | Practice |
|---|---|---|---|
| Adapter | Make an object with the wrong interface fit the interface you need, by wrapping it. | ★ | Exercise |
| Composite | Treat a single object and a group of objects the same way, by giving both one interface. | ★ | Run it |
| Decorator | Add behavior to an object by wrapping it in another object with the same interface, instead of creating a subclass for every combination. | ★ | Exercise |
| Delegation | An object hands part of its work to a helper object. | ★ | Run it |
| Facade | One simple entry point in front of a group of complicated classes. | ★ | Exercise |
| Module | Group related code behind one public surface and hide the rest. | ★ | Run it |
| Proxy | A stand-in with the same interface as the real object, placed in front of it to add control: caching, lazy loading, access checks, logging. | ★ | Exercise |
| Bridge | Split a thing into two separate hierarchies, an abstraction and an implementation, joined by a reference, so each can vary on its own. | Run it | |
| Extension object | Add new behavior to a type without changing it. | Run it | |
| Flyweight | Share the heavy, repeated part of many objects (the same tree type, the same icon) instead of storing a copy in each one. | Run it | |
| Marker | An interface or annotation with no methods that only tags a class, so other code can treat tagged classes specially. | Run it |
Behavioral16 patterns, 9 popular
| Pattern | In one line | Popular | Practice |
|---|---|---|---|
| Chain of responsibility | Pass a request along a chain of handlers. | ★ | Exercise |
| Command | Turn an action into an object with execute() (and often undo()). | ★ | Exercise |
| Fluent interface | Design an API so calls chain into readable, sentence-like code, usually by returning the same object from each method. | ★ | Run it |
| Iterator | Walk through a collection one item at a time without knowing how it is stored inside. | ★ | Run it |
| Null object | Instead of returning null and checking everywhere, return a harmless object that does nothing, so callers never need an if. | ★ | Exercise |
| Observer | An object (the subject) keeps a list of listeners and tells them whenever something changes, without knowing who they are. | ★ | Exercise |
| State | Represent each state as its own object that knows what may happen next, instead of one big when over a status field. | ★ | Exercise |
| Strategy | Put each interchangeable algorithm behind one small interface and let the caller choose which one to use. | ★ | Exercise |
| Template method | A base class fixes the order of the steps of an algorithm; subclasses fill in some of the steps. | ★ | Exercise |
| Blackboard | Several independent experts read and write a shared workspace (the blackboard). | Run it | |
| Interpreter | Represent a small language as a tree of objects and give each node an evaluate step. | Run it | |
| Mediator | Objects do not talk to each other directly. | Run it | |
| Memento | Capture an object’s state in a snapshot so you can restore it later (undo), without exposing its insides. | Run it | |
| Servant | Put behavior that many unrelated classes need into one helper class (the servant), instead of copying it into each of them. | Run it | |
| Specification | Express business rules as small objects that answer yes or no, then combine them with and, or, not. | Run it | |
| Visitor | Add new operations to a family of classes without editing them, by letting a visitor object handle each class. | Run it |
Concurrency16 patterns, 3 popular
| Pattern | In one line | Popular | Practice |
|---|---|---|---|
| Lock | Only one thread at a time may run a critical section; the others wait their turn. | ★ | Run it |
| Scheduler | Decide when and on which thread tasks run: now, later, repeatedly, on a pool. | ★ | Run it |
| Thread pool | Keep a fixed number of worker threads and feed them tasks from a queue, instead of creating a new thread for each task. | ★ | Run it |
| Active object | An object owns its own thread and a queue. | Run it | |
| Balking | If an object is not in the right state for a request (already busy), the request is ignored instead of waiting. | Run it | |
| Binding properties | Link two properties so that when one changes, the other follows automatically. | Run it | |
| Double-checked locking | Check a condition without a lock first (fast path), and only lock and check again if work is really needed. | Run it | |
| Event-based asynchronous | Start work in the background and get notified by an event (callback) when it finishes or fails, instead of waiting for it. | Run it | |
| Guarded suspension | A thread waits until a condition is true (a message has arrived) before it continues. | Run it | |
| Join | Wait until several concurrent tasks have all finished before continuing. | Run it | |
| Messaging design pattern (MDP) | Components communicate by sending messages into a queue instead of calling each other directly or sharing memory. | Run it | |
| Monitor object | An object whose methods are all mutually exclusive, so only one thread is inside it at a time. | Run it | |
| Reactor | One event loop waits for events and dispatches each to the handler registered for it. | Run it | |
| Read-write lock | Many threads may read at the same time, but a writer needs exclusive access. | Run it | |
| Thread-specific storage | Give each thread its own private copy of a value, so threads do not need locks to share it. | Run it | |
| CPU atomic operation | Use a single indivisible hardware-supported step (like compare-and-set) so threads can update a value safely without locks. | Run it |
2. Creational patterns
How objects get made. These patterns hide the details of creation so the rest of the code does not depend on concrete classes.
Abstract factory ★ Popular
A factory of factories: one interface that creates a whole family of related objects, so you can swap the entire family (light theme widgets, dark theme widgets) at once.
The idea in Kotlin (a complete program you can run)
interface Button { fun render(): String }
interface Checkbox { fun render(): String }
interface WidgetFactory { fun button(): Button; fun checkbox(): Checkbox }
class LightButton : Button { override fun render() = "[ light button ]" }
class LightCheckbox : Checkbox { override fun render() = "[ ] light checkbox" }
class DarkButton : Button { override fun render() = "[ dark button ]" }
class DarkCheckbox : Checkbox { override fun render() = "[x] dark checkbox" }
class LightFactory : WidgetFactory {
override fun button() = LightButton()
override fun checkbox() = LightCheckbox()
}
class DarkFactory : WidgetFactory {
override fun button() = DarkButton()
override fun checkbox() = DarkCheckbox()
}
fun screen(factory: WidgetFactory) = println(factory.button().render() + " " + factory.checkbox().render())
fun main() {
screen(LightFactory())
screen(DarkFactory()) // the whole family changes together
}
O: Open/Closed · D: Dependency Inversion
New families are added as new factories (O) and screens depend only on the factory interface (D).
In Android and Kotlin: Theme or platform kits, a DataSourceFactory that returns matching local and remote classes for each build flavor.
Run it: Abstract factory
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Builder ★ Popular
Build a complex object step by step, then validate and produce it in one go. Kotlin gives you named arguments, default values and lambda-with-receiver DSLs.
The idea in Kotlin
data class Request(val url: String, val method: String, val headers: Map<String, String>)
class RequestBuilder {
var url = ""
var method = "GET"
private val headers = mutableMapOf<String, String>()
fun header(key: String, value: String) { headers[key] = value }
fun build(): Request {
require(url.isNotBlank()) { "url is required" }
return Request(url, method, headers.toMap())
}
}
fun request(init: RequestBuilder.() -> Unit) = RequestBuilder().apply(init).build()
val r = request { url = "https://example.com"; header("Accept", "application/json") }
Construction rules live in the builder, not scattered across callers: Single Responsibility (S). The finished object stays immutable.
In Android and Kotlin: Retrofit.Builder(), AlertDialog.Builder, NotificationCompat.Builder, Room.databaseBuilder.
Try it: Builder
Fill in header, build and the request { } DSL function. build() must reject a blank url with require. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.
data class Request(val url: String, val method: String, val headers: Map<String, String>)
class RequestBuilder {
var url = ""
var method = "GET"
private val headers = mutableMapOf<String, String>()
fun header(key: String, value: String) { TODO("remember the header") }
fun build(): Request = TODO("check the url, then create the Request")
}
fun request(init: RequestBuilder.() -> Unit): Request = TODO("create a builder, run init on it, build")
Open in Kotlin Playground ↗
Hint
RequestBuilder().apply(init) runs your lambda on a new builder, then call build() on the result. headers.toMap() makes an immutable copy.
See the tests
fun main() {
test("builds with defaults") {
val r = request { url = "https://x.dev" }
check(r == Request("https://x.dev", "GET", emptyMap())) { "got $r" }
}
test("keeps headers and method") {
val r = request { url = "https://x.dev"; method = "POST"; header("Accept", "json") }
check(r == Request("https://x.dev", "POST", mapOf("Accept" to "json"))) { "got $r" }
}
test("blank url is rejected") {
val problem = runCatching { request { } }.exceptionOrNull()
check(problem is IllegalArgumentException) { "expected IllegalArgumentException, got $problem" }
}
}
Show a solution
fun header(key: String, value: String) { headers[key] = value }
fun build(): Request {
require(url.isNotBlank()) { "url is required" }
return Request(url, method, headers.toMap())
}
}
fun request(init: RequestBuilder.() -> Unit): Request = RequestBuilder().apply(init).build()
Where the solution shows only part of the code, replace the matching part of the starter.
Dependency injection ★ Popular
A class does not create what it needs. Its dependencies are handed in from outside, usually through the constructor, so you can give it real or fake ones.
The idea in Kotlin
interface Clock { fun now(): Long }
interface Sink { fun write(line: String) }
class ReportService(private val clock: Clock, private val sink: Sink) {
fun publish(title: String) = sink.write("$title @ ${clock.now()}")
}
object SystemClock : Clock { override fun now() = System.currentTimeMillis() }
object ConsoleSink : Sink { override fun write(line: String) = println(line) }
fun main() {
ReportService(SystemClock, ConsoleSink).publish("Daily") // wired once, at the edge
}
This is Dependency Inversion (D) made practical: depend on Clock and Sink interfaces, receive the real ones at the edge of the app.
In Android and Kotlin: Hilt, Dagger, Koin. The architecture series covers them in Part 6.
Try it: Dependency injection
Make ReportService write "<title> @ <time>" to the sink, using only the injected clock (never System.currentTimeMillis()). Then the tests can control time. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.
interface Clock { fun now(): Long }
interface Sink { fun write(line: String) }
class ReportService(private val clock: Clock, private val sink: Sink) {
fun publish(title: String) { TODO("write \"<title> @ <time>\" to the sink using the injected clock") }
}
Open in Kotlin Playground ↗
Hint
Use string templates: "$title @ ${clock.now()}". The point is that time and output come from the constructor, so the test supplies fakes.
See the tests
class FakeClock(private val t: Long) : Clock { override fun now() = t }
class ListSink : Sink {
val lines = mutableListOf<String>()
override fun write(line: String) { lines.add(line) }
}
fun main() {
test("writes title and injected time") {
val sink = ListSink()
ReportService(FakeClock(1000), sink).publish("Daily")
check(sink.lines == listOf("Daily @ 1000")) { "got ${sink.lines}" }
}
test("a different clock gives a different line") {
val sink = ListSink()
ReportService(FakeClock(42), sink).publish("Weekly")
check(sink.lines == listOf("Weekly @ 42")) { "got ${sink.lines}" }
}
}
Show a solution
class ReportService(private val clock: Clock, private val sink: Sink) {
fun publish(title: String) { sink.write("$title @ ${clock.now()}") }
}
Where the solution shows only part of the code, replace the matching part of the starter.
Factory ★ Popular
One place decides which class to create, so the rest of the code only knows the interface.
The idea in Kotlin
interface Notifier { fun send(message: String): String }
class EmailNotifier : Notifier { override fun send(message: String) = "email:$message" }
class SmsNotifier : Notifier { override fun send(message: String) = "sms:$message" }
object NotifierFactory {
fun create(kind: String): Notifier = when (kind) {
"email" -> EmailNotifier()
"sms" -> SmsNotifier()
else -> throw IllegalArgumentException("unknown kind: $kind")
}
}
O: Open/Closed · D: Dependency Inversion
The client depends on the Notifier interface (D), and adding a new kind touches only the factory (O at the construction point).
In Android and Kotlin: ViewModelProvider.Factory, Fragment factories, creating the right repository for a build flavor.
Try it: Factory
Finish NotifierFactory.create so it returns the right notifier for "email", "sms" and "push", and throws IllegalArgumentException for anything else. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.
interface Notifier { fun send(message: String): String }
class EmailNotifier : Notifier { override fun send(message: String) = "email:$message" }
class SmsNotifier : Notifier { override fun send(message: String) = "sms:$message" }
class PushNotifier : Notifier { override fun send(message: String) = "push:$message" }
object NotifierFactory {
fun create(kind: String): Notifier = TODO("pick the class for this kind")
}
Open in Kotlin Playground ↗
Hint
Use a when (kind) expression with one branch per kind, and an else that throws.
See the tests
fun main() {
test("email") { check(NotifierFactory.create("email").send("hi") == "email:hi") { "wrong notifier" } }
test("sms") { check(NotifierFactory.create("sms").send("hi") == "sms:hi") { "wrong notifier" } }
test("push") { check(NotifierFactory.create("push").send("hi") == "push:hi") { "wrong notifier" } }
test("unknown kind is rejected") {
val problem = runCatching { NotifierFactory.create("fax") }.exceptionOrNull()
check(problem is IllegalArgumentException) { "expected IllegalArgumentException, got $problem" }
}
}
Show a solution
object NotifierFactory {
fun create(kind: String): Notifier = when (kind) {
"email" -> EmailNotifier()
"sms" -> SmsNotifier()
"push" -> PushNotifier()
else -> throw IllegalArgumentException("unknown kind: $kind")
}
}
Where the solution shows only part of the code, replace the matching part of the starter.
Lazy initialization ★ Popular
Delay creating something expensive until the first time it is really needed, then remember it.
The idea in Kotlin (a complete program you can run)
class Heavy {
init { println("Heavy created") }
fun hello() = "hello"
}
class Screen {
val heavy by lazy { Heavy() } // not created yet
}
fun main() {
val screen = Screen()
println("screen created")
println(screen.heavy.hello()) // created here, on first use
println(screen.heavy.hello()) // reused
}
S: Single Responsibility · D: Dependency Inversion
Expensive setup lives behind the property (S); callers just use the value.
In Android and Kotlin: Kotlin has it built in: by lazy { }, plus lateinit var for values set later.
Run it: Lazy initialization
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Singleton ★ Popular
Exactly one instance of a class, reachable from everywhere. In Kotlin you write object and the language does the rest.
The idea in Kotlin
object Logger { // one instance, created on first use
fun log(msg: String) = println("[LOG] $msg")
}
Logger.log("started") // no constructor, no getInstance()
// Better for testing: depend on an interface, make ONE implementation by DI
interface Clock { fun now(): Long }
object SystemClock : Clock { override fun now() = System.currentTimeMillis() }
D: Dependency Inversion · S: Single Responsibility
Singleton is the pattern to be careful with. A hidden global breaks Dependency Inversion (D): code that calls IdGenerator.next() directly cannot be given a fake in tests. Prefer to inject it through an interface and let DI (Hilt, Koin) make it single.
In Android and Kotlin: Application-scoped objects such as a database or an HTTP client (@Singleton in Hilt).
Try it: Singleton
Implement next() so ids count 1, 2, 3 across the whole program, and reset() so tests can start clean. Notice that you needed reset() at all: that is the cost of global state. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.
object IdGenerator {
private var last = 0
fun next(): Int = TODO("return 1, then 2, then 3, ...")
fun reset() { TODO("start counting from the beginning again") }
}
Open in Kotlin Playground ↗
Hint
++last increases the number and returns the new value. reset() sets it back to 0.
See the tests
fun main() {
test("counts up from 1") {
IdGenerator.reset()
check(IdGenerator.next() == 1) { "first id should be 1" }
check(IdGenerator.next() == 2) { "second id should be 2" }
}
test("every caller shares one counter") {
IdGenerator.reset()
val fromA = IdGenerator.next()
val fromB = IdGenerator.next()
check(fromB == fromA + 1) { "callers do not share one counter" }
}
test("reset starts again") {
IdGenerator.next(); IdGenerator.next()
IdGenerator.reset()
check(IdGenerator.next() == 1) { "reset did not restart the count" }
}
}
Show a solution
object IdGenerator {
private var last = 0
fun next(): Int = ++last
fun reset() { last = 0 }
}
Where the solution shows only part of the code, replace the matching part of the starter.
Multiton
Like Singleton, but one instance per key: a map of named, shared instances.
The idea in Kotlin (a complete program you can run)
class Logger private constructor(val name: String) {
companion object {
private val instances = mutableMapOf<String, Logger>()
fun of(name: String): Logger = instances.getOrPut(name) { Logger(name) }
}
fun log(message: String) = println("[$name] $message")
}
fun main() {
Logger.of("net").log("connected")
println(Logger.of("net") === Logger.of("net")) // true: same key, same instance
println(Logger.of("net") === Logger.of("db")) // false: different key
}
Same caution as Singleton: shared global state is hard to fake in tests, so hide it behind an interface (D).
In Android and Kotlin: A logger per tag, one DataStore per file name, one database per user.
Run it: Multiton
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Object pool
Keep a set of ready-made, reusable objects and lend them out, instead of creating and destroying them again and again.
The idea in Kotlin (a complete program you can run)
class Pool<T>(size: Int, create: () -> T) {
private val free = ArrayDeque<T>().apply { repeat(size) { addLast(create()) } }
fun <R> use(block: (T) -> R): R {
val item = free.removeFirstOrNull() ?: error("pool exhausted")
try {
return block(item)
} finally {
free.addLast(item) // always give it back
}
}
}
fun main() {
var created = 0
val pool = Pool(2) { created++; StringBuilder() }
repeat(5) { i ->
pool.use { sb ->
sb.setLength(0)
sb.append("job ").append(i)
println(sb)
}
}
println("objects created: $created") // 2, not 5
}
S: Single Responsibility · D: Dependency Inversion
The pool owns reuse (S); users borrow through a small interface.
In Android and Kotlin: RecyclerView view holders and its RecycledViewPool, bitmap pools in image loaders, database connection pools, Message.obtain().
Run it: Object pool
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Prototype
Create new objects by copying an existing one and changing a few fields, instead of building from scratch.
The idea in Kotlin (a complete program you can run)
data class Enemy(val name: String, val hp: Int, val tags: List<String>)
fun main() {
val goblin = Enemy("Goblin", 10, listOf("green", "small"))
val chief = goblin.copy(name = "Goblin Chief", hp = 30)
println(goblin)
println(chief)
println(goblin.tags === chief.tags) // true: the list itself is shared (shallow copy)
}
New variants appear by copying, with no new class needed (O).
In Android and Kotlin: Kotlin data class gives you copy() for free. Remember it is a shallow copy.
Run it: Prototype
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Resource acquisition is initialization (RAII)
Tie a resource (file, connection, lock) to the lifetime of an object, so it is released automatically even when something fails.
The idea in Kotlin (a complete program you can run)
class Connection(val name: String) : AutoCloseable {
init { println("open $name") }
fun query() = "result from $name"
override fun close() = println("close $name")
}
fun main() {
Connection("db").use { println(it.query()) } // closed after the block
runCatching { Connection("cache").use { error("boom") } }
.onFailure { println("caught: ${it.message}") } // still closed, even on error
}
Cleanup is the resource’s job, not scattered across callers (S).
In Android and Kotlin: Kotlin’s use { } on Closeable/AutoCloseable, lock.withLock { }, Room transactions.
Run it: Resource acquisition is initialization (RAII)
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗3. Structural patterns
How objects are put together. These patterns combine classes and objects into larger structures while keeping them flexible.
Adapter ★ Popular
Make an object with the wrong interface fit the interface you need, by wrapping it. Also called Wrapper or Translator.
The idea in Kotlin
interface Thermometer { fun celsius(): Double }
class LegacySensor(private val fahrenheit: Double) { fun readFahrenheit() = fahrenheit }
class SensorAdapter(private val legacy: LegacySensor) : Thermometer {
override fun celsius() = (legacy.readFahrenheit() - 32) * 5 / 9
}
D: Dependency Inversion · O: Open/Closed
Your code depends on Thermometer (D); the old sensor is reused without being edited (O).
In Android and Kotlin: RecyclerView.Adapter, mappers from API DTOs to domain models, wrapping a third-party SDK.
Try it: Adapter
Write SensorAdapter.celsius() using the formula (f - 32) * 5 / 9. Do not change LegacySensor. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.
interface Thermometer { fun celsius(): Double }
// Imagine this class comes from an old library. Do NOT edit it.
class LegacySensor(private val fahrenheit: Double) {
fun readFahrenheit(): Double = fahrenheit
}
class SensorAdapter(private val legacy: LegacySensor) : Thermometer {
override fun celsius(): Double = TODO("convert Fahrenheit to Celsius")
}
Open in Kotlin Playground ↗
Hint
Call legacy.readFahrenheit() and apply the formula. The adapter implements the interface you want and holds the thing you have.
See the tests
fun main() {
test("boiling point") { check(same(SensorAdapter(LegacySensor(212.0)).celsius(), 100.0)) { "212F should be 100C" } }
test("freezing point") { check(same(SensorAdapter(LegacySensor(32.0)).celsius(), 0.0)) { "32F should be 0C" } }
test("usable as a Thermometer") {
val t: Thermometer = SensorAdapter(LegacySensor(50.0))
check(same(t.celsius(), 10.0)) { "50F should be 10C" }
}
}
Show a solution
class SensorAdapter(private val legacy: LegacySensor) : Thermometer {
override fun celsius(): Double = (legacy.readFahrenheit() - 32) * 5 / 9
}
Where the solution shows only part of the code, replace the matching part of the starter.
Composite ★ Popular
Treat a single object and a group of objects the same way, by giving both one interface. Groups contain items or other groups, forming a tree.
The idea in Kotlin (a complete program you can run)
interface FileNode { val name: String; fun size(): Int }
class FileLeaf(override val name: String, private val bytes: Int) : FileNode {
override fun size() = bytes
}
class Folder(override val name: String, private val children: List<FileNode>) : FileNode {
override fun size() = children.sumOf { it.size() } // works for files AND folders
}
fun main() {
val tree = Folder("root", listOf(
FileLeaf("a.txt", 10),
Folder("img", listOf(FileLeaf("b.png", 200), FileLeaf("c.png", 300)))
))
println("total size = ${tree.size()}") // 510
}
O: Open/Closed · L: Liskov Substitution
Folders and files are interchangeable FileNodes (L); you can add node types without touching callers (O).
In Android and Kotlin: Android View and ViewGroup, the Jetpack Compose tree, menus with submenus, file trees.
Run it: Composite
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Decorator ★ Popular
Add behavior to an object by wrapping it in another object with the same interface, instead of creating a subclass for every combination.
The idea in Kotlin
interface Coffee { fun cost(): Double; fun description(): String }
class Espresso : Coffee { override fun cost() = 2.0; override fun description() = "espresso" }
class Milk(private val inner: Coffee) : Coffee {
override fun cost() = inner.cost() + 0.5
override fun description() = inner.description() + " + milk"
}
val order: Coffee = Milk(Espresso())
O: Open/Closed · L: Liskov Substitution · S: Single Responsibility
New features arrive as new wrappers, never as edits (O). Every wrapper can stand in for the original (L), and each does one small job (S).
In Android and Kotlin: OkHttp interceptors, ContextWrapper, a repository wrapped with logging or caching.
Try it: Decorator
Complete Milk (+0.5) and Sugar (+0.2). Each must wrap another Coffee and extend its cost and description. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.
interface Coffee { fun cost(): Double; fun description(): String }
class Espresso : Coffee {
override fun cost(): Double = 2.0
override fun description(): String = "espresso"
}
class Milk(private val inner: Coffee) : Coffee {
override fun cost(): Double = TODO("inner cost plus 0.5")
override fun description(): String = TODO("inner description followed by + milk")
}
class Sugar(private val inner: Coffee) : Coffee {
override fun cost(): Double = TODO("inner cost plus 0.2")
override fun description(): String = TODO("inner description followed by + sugar")
}
Open in Kotlin Playground ↗
Hint
Always start from inner.cost() and inner.description(), then add your part.
See the tests
fun main() {
test("plain espresso") {
check(same(Espresso().cost(), 2.0)) { "wrong cost" }
}
test("milk adds cost and text") {
val c = Milk(Espresso())
check(same(c.cost(), 2.5)) { "wrong cost ${c.cost()}" }
check(c.description() == "espresso + milk") { "wrong text '${c.description()}'" }
}
test("wrappers stack in any order") {
val c = Milk(Sugar(Espresso()))
check(same(c.cost(), 2.7)) { "wrong cost ${c.cost()}" }
check(c.description() == "espresso + sugar + milk") { "wrong text '${c.description()}'" }
}
}
Show a solution
class Milk(private val inner: Coffee) : Coffee {
override fun cost(): Double = inner.cost() + 0.5
override fun description(): String = inner.description() + " + milk"
}
class Sugar(private val inner: Coffee) : Coffee {
override fun cost(): Double = inner.cost() + 0.2
override fun description(): String = inner.description() + " + sugar"
}
Where the solution shows only part of the code, replace the matching part of the starter.
Delegation ★ Popular
An object hands part of its work to a helper object. Kotlin supports this in the language with the by keyword.
The idea in Kotlin (a complete program you can run)
interface Printer { fun print(text: String) }
class ConsolePrinter : Printer { override fun print(text: String) = println(text) }
class Report(printer: Printer) : Printer by printer // every call is forwarded
class TimestampPrinter(private val inner: Printer) : Printer by inner {
override fun print(text: String) = inner.print("[12:00] $text") // override only what changes
}
fun main() {
Report(ConsolePrinter()).print("plain")
TimestampPrinter(ConsolePrinter()).print("stamped")
}
O: Open/Closed · S: Single Responsibility · D: Dependency Inversion
Prefer composition over inheritance: reuse behavior by delegating (O) to an interface (D), without a fragile base class.
In Android and Kotlin: class Foo : Bar by impl, property delegates (by lazy, by viewModels(), by Delegates.observable).
Run it: Delegation
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Facade ★ Popular
One simple entry point in front of a group of complicated classes. Callers make one call; the facade coordinates the rest.
The idea in Kotlin
class Inventory { fun reserve(item: String) = item != "ghost" }
class Payment { fun charge(amount: Double) = amount > 0 }
class Shipping { fun schedule(item: String) = "ship:$item" }
class OrderFacade(private val inventory: Inventory, private val payment: Payment, private val shipping: Shipping) {
fun order(item: String, amount: Double): String {
if (!inventory.reserve(item)) return "failed"
if (!payment.charge(amount)) return "failed"
return shipping.schedule(item)
}
}
S: Single Responsibility · I: Interface Segregation
Callers depend on one small interface (I), and the coordination lives in one place (S).
In Android and Kotlin: A repository hiding a database and an API, a UseCase hiding several repositories, an SDK wrapper such as AnalyticsManager.
Try it: Facade
Complete OrderFacade.order: reserve the item, charge the amount, then schedule shipping and return its code. If a step fails, return "failed" and do not continue. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.
class Inventory { fun reserve(item: String): Boolean = item != "ghost" }
class Payment { fun charge(amount: Double): Boolean = amount > 0 }
class Shipping { fun schedule(item: String): String = "ship:$item" }
class OrderFacade(
private val inventory: Inventory,
private val payment: Payment,
private val shipping: Shipping,
) {
fun order(item: String, amount: Double): String =
TODO("reserve, charge, then schedule shipping; return the shipping code or \"failed\"")
}
Open in Kotlin Playground ↗
Hint
Use early returns: stop at the first failing step so shipping is never scheduled for an unpaid order.
See the tests
fun main() {
val facade = OrderFacade(Inventory(), Payment(), Shipping())
test("successful order returns the shipping code") {
check(facade.order("pen", 2.0) == "ship:pen") { "wrong result" }
}
test("unknown item fails") {
check(facade.order("ghost", 2.0) == "failed") { "expected failed" }
}
test("payment failure fails") {
check(facade.order("pen", 0.0) == "failed") { "expected failed" }
}
}
Show a solution
fun order(item: String, amount: Double): String {
if (!inventory.reserve(item)) return "failed"
if (!payment.charge(amount)) return "failed"
return shipping.schedule(item)
}
Where the solution shows only part of the code, replace the matching part of the starter.
Module ★ Popular
Group related code behind one public surface and hide the rest. Callers see a small API, not the internals.
The idea in Kotlin (a complete program you can run)
object MathModule {
private const val TWO_PI = 6.283185307179586 // hidden detail
fun circumference(radius: Double) = TWO_PI * radius
fun area(radius: Double) = Math.PI * radius * radius
}
fun main() {
println(MathModule.circumference(1.0))
println(MathModule.area(2.0))
// MathModule.TWO_PI would not compile: it is private
}
S: Single Responsibility · I: Interface Segregation · D: Dependency Inversion
One module, one purpose (S), a small public API (I), and others depend on the API, not the insides (D).
In Android and Kotlin: Kotlin object, internal and private visibility, Gradle modules (see architecture Part 5).
Run it: Module
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Proxy ★ Popular
A stand-in with the same interface as the real object, placed in front of it to add control: caching, lazy loading, access checks, logging.
The idea in Kotlin
interface ImageLoader { fun load(url: String): String }
class GlideProxy : ImageLoader { // hides the library
override fun load(url: String) = "Glide loaded $url"
}
class LoggingLoader(private val real: ImageLoader) : ImageLoader {
override fun load(url: String): String {
println("loading $url")
return real.load(url)
}
}
L: Liskov Substitution · D: Dependency Inversion
A proxy must behave like the real thing (L), and callers depend only on the interface (D). This is the pattern behind ViewProxy and GlideProxy in my architecture series.
In Android and Kotlin: ViewProxy, GlideProxy (wrapping Glide behind an ImageLoader), by lazy, Retrofit’s generated service.
Try it: Proxy
Make CachingLoader ask the real loader only the first time a url is requested, and return the saved result afterwards. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.
interface ImageLoader { fun load(url: String): String }
class NetworkLoader : ImageLoader {
var calls = 0
override fun load(url: String): String { calls++; return "bytes of $url" }
}
class CachingLoader(private val real: ImageLoader) : ImageLoader {
private val cache = mutableMapOf<String, String>()
override fun load(url: String): String = TODO("use the cache, call real only on a miss")
}
Open in Kotlin Playground ↗
Hint
getOrPut(key) { value } returns the stored value, or computes it, stores it and returns it.
See the tests
fun main() {
test("returns the real result") {
val proxy = CachingLoader(NetworkLoader())
check(proxy.load("a.png") == "bytes of a.png") { "wrong result" }
}
test("asks the real loader once per url") {
val real = NetworkLoader()
val proxy = CachingLoader(real)
proxy.load("a.png"); proxy.load("a.png"); proxy.load("a.png")
check(real.calls == 1) { "real loader was called ${real.calls} times" }
}
test("different urls are cached separately") {
val real = NetworkLoader()
val proxy = CachingLoader(real)
proxy.load("a.png"); proxy.load("b.png"); proxy.load("a.png")
check(real.calls == 2) { "real loader was called ${real.calls} times" }
}
}
Show a solution
class CachingLoader(private val real: ImageLoader) : ImageLoader {
private val cache = mutableMapOf<String, String>()
override fun load(url: String): String = cache.getOrPut(url) { real.load(url) }
}
Where the solution shows only part of the code, replace the matching part of the starter.
Bridge
Split a thing into two separate hierarchies, an abstraction and an implementation, joined by a reference, so each can vary on its own.
The idea in Kotlin (a complete program you can run)
interface Renderer { fun drawCircle(radius: Int): String }
class VectorRenderer : Renderer { override fun drawCircle(radius: Int) = "vector circle r=$radius" }
class RasterRenderer : Renderer { override fun drawCircle(radius: Int) = "raster circle r=$radius px" }
abstract class Shape(protected val renderer: Renderer) { abstract fun draw(): String }
class Circle(private val radius: Int, renderer: Renderer) : Shape(renderer) {
override fun draw() = renderer.drawCircle(radius)
}
fun main() {
println(Circle(5, VectorRenderer()).draw())
println(Circle(5, RasterRenderer()).draw()) // new renderer, no new Shape class
}
O: Open/Closed · D: Dependency Inversion · S: Single Responsibility
Shapes and renderers change independently (O, S) and meet through an interface (D).
In Android and Kotlin: A View drawing through a Canvas, a repository using any DataSource, UI code with a swappable Logger.
Run it: Bridge
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Extension object
Add new behavior to a type without changing it. In Kotlin the natural tool is the extension function.
The idea in Kotlin (a complete program you can run)
data class User(val first: String, val last: String)
fun User.fullName() = "$first $last" // User is unchanged
fun String.initials() = split(" ").joinToString("") { it.take(1) }
fun main() {
val user = User("Ada", "Lovelace")
println(user.fullName())
println(user.fullName().initials())
}
Add behavior from outside, leaving the original class untouched (O).
In Android and Kotlin: fun String.toSlug(), View.show(), Android KTX (core-ktx) is a big collection of extensions.
Run it: Extension object
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Flyweight
Share the heavy, repeated part of many objects (the same tree type, the same icon) instead of storing a copy in each one.
The idea in Kotlin (a complete program you can run)
data class TreeType(val name: String, val color: String) // imagine big textures here
object TreeTypes {
private val cache = mutableMapOf<String, TreeType>()
fun get(name: String, color: String) = cache.getOrPut(name + color) { TreeType(name, color) }
}
class Tree(val x: Int, val y: Int, val type: TreeType)
fun main() {
val forest = (1..1000).map {
Tree(it, it * 2, TreeTypes.get(if (it % 2 == 0) "oak" else "pine", "green"))
}
println("trees: ${forest.size}")
println("distinct tree types: ${forest.map { it.type }.distinct().size}") // 2
}
Shared data is separated from per-object data (S).
In Android and Kotlin: String interning, bitmap caches, text styles shared by many spans, Integer.valueOf cache.
Run it: Flyweight
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Marker
An interface or annotation with no methods that only tags a class, so other code can treat tagged classes specially.
The idea in Kotlin (a complete program you can run)
interface Sensitive // marker: nothing to implement
data class Password(val value: String) : Sensitive
data class City(val name: String)
fun log(item: Any) = println(if (item is Sensitive) "<hidden>" else item.toString())
fun main() {
log(City("Tokyo"))
log(Password("hunter2"))
}
Add a tag instead of editing the code that reacts to it (O).
In Android and Kotlin: Serializable, RandomAccess, custom annotations such as @Composable or @VisibleForTesting.
Run it: Marker
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗4. Behavioral patterns
How objects talk to each other and share work. These patterns assign responsibilities and manage communication.
Chain of responsibility ★ Popular
Pass a request along a chain of handlers. Each one either handles it or passes it to the next.
The idea in Kotlin
abstract class Handler(private val next: Handler?) {
fun handle(request: String): String = if (canHandle(request)) name() else next?.handle(request) ?: "unhandled"
protected abstract fun canHandle(request: String): Boolean
protected abstract fun name(): String
}
class Auth(next: Handler?) : Handler(next) {
override fun canHandle(request: String) = request == "login"
override fun name() = "auth"
}
class Logging : Handler(null) {
override fun canHandle(request: String) = true
override fun name() = "logging"
}
O: Open/Closed · S: Single Responsibility · L: Liskov Substitution
Handlers are added or reordered without editing others (O), and each decides only its own case (S).
In Android and Kotlin: OkHttp interceptors, touch event dispatch through views, try/catch ladders, middleware, validation steps.
Try it: Chain of responsibility
Complete Approver.approve: approve at this level (return its title) when the amount is within its limit; otherwise pass the request to next. If nobody is left, return "rejected". Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.
abstract class Approver(private val next: Approver?) {
abstract val limit: Int
abstract val title: String
fun approve(amount: Int): String =
TODO("return title if amount <= limit, else ask next, else \"rejected\"")
}
class TeamLead(next: Approver?) : Approver(next) { override val limit = 100; override val title = "lead" }
class Manager(next: Approver?) : Approver(next) { override val limit = 1000; override val title = "manager" }
class Director : Approver(null) { override val limit = 10000; override val title = "director" }
Open in Kotlin Playground ↗
Hint
next?.approve(amount) ?: "rejected" asks the next handler if there is one, and falls back to "rejected" at the end of the chain.
See the tests
fun main() {
val chain = TeamLead(Manager(Director()))
test("small amount is handled by the lead") { check(chain.approve(50) == "lead") { "got ${chain.approve(50)}" } }
test("medium amount goes to the manager") { check(chain.approve(500) == "manager") { "got ${chain.approve(500)}" } }
test("large amount goes to the director") { check(chain.approve(5000) == "director") { "got ${chain.approve(5000)}" } }
test("too large is rejected") { check(chain.approve(50000) == "rejected") { "got ${chain.approve(50000)}" } }
}
Show a solution
fun approve(amount: Int): String =
if (amount <= limit) title else next?.approve(amount) ?: "rejected"
Where the solution shows only part of the code, replace the matching part of the starter.
Command ★ Popular
Turn an action into an object with execute() (and often undo()). You can then queue it, log it, retry it or undo it.
The idea in Kotlin
interface Command { fun execute(); fun undo() }
class Editor { var text = "" }
class Append(private val editor: Editor, private val extra: String) : Command {
override fun execute() { editor.text += extra }
override fun undo() { editor.text = editor.text.removeSuffix(extra) }
}
S: Single Responsibility · O: Open/Closed · D: Dependency Inversion
The invoker does not know what the action does (D); each command is one small class (S); new commands are added without edits (O).
In Android and Kotlin: Undo snackbars, WorkManager work requests, Runnable/Handler.post, MVI intents/actions (Part 4 of the architecture series).
Try it: Command
Finish History: run executes a command and remembers it, undo reverses the most recent one (and does nothing when there is nothing to undo). Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.
class Editor { var text = "" }
interface Command { fun execute(); fun undo() }
class Append(private val editor: Editor, private val extra: String) : Command {
override fun execute() { editor.text += extra }
override fun undo() { editor.text = editor.text.removeSuffix(extra) }
}
class History {
private val done = ArrayDeque<Command>()
fun run(command: Command) { TODO("execute it and remember it") }
fun undo() { TODO("undo the most recent command, or do nothing if there is none") }
}
Open in Kotlin Playground ↗
Hint
removeLastOrNull() returns null on an empty list, and ?.undo() then does nothing.
See the tests
fun main() {
test("run applies commands in order") {
val editor = Editor(); val history = History()
history.run(Append(editor, "Hello"))
history.run(Append(editor, " world"))
check(editor.text == "Hello world") { "got '${editor.text}'" }
}
test("undo reverses the latest command") {
val editor = Editor(); val history = History()
history.run(Append(editor, "Hello"))
history.run(Append(editor, " world"))
history.undo()
check(editor.text == "Hello") { "got '${editor.text}'" }
history.undo()
check(editor.text == "") { "got '${editor.text}'" }
}
test("undo with nothing to undo is safe") {
History().undo()
}
}
Show a solution
class History {
private val done = ArrayDeque<Command>()
fun run(command: Command) {
command.execute()
done.addLast(command)
}
fun undo() {
done.removeLastOrNull()?.undo()
}
}
Where the solution shows only part of the code, replace the matching part of the starter.
Fluent interface ★ Popular
Design an API so calls chain into readable, sentence-like code, usually by returning the same object from each method.
The idea in Kotlin (a complete program you can run)
class Query {
private val parts = mutableListOf<String>()
fun from(table: String) = apply { parts += "FROM $table" }
fun where(condition: String) = apply { parts += "WHERE $condition" }
fun limit(n: Int) = apply { parts += "LIMIT $n" }
override fun toString() = "SELECT * " + parts.joinToString(" ")
}
fun main() {
println(Query().from("users").where("age > 18").limit(10))
}
S: Single Responsibility · I: Interface Segregation
Each method does one small step (S) and the chain exposes only what is needed (I).
In Android and Kotlin: Builders, Retrofit.Builder().baseUrl().build(), Kotlin apply, collection chains such as list.filter{}.map{}, Room and query DSLs.
Run it: Fluent interface
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Iterator ★ Popular
Walk through a collection one item at a time without knowing how it is stored inside.
The idea in Kotlin (a complete program you can run)
class Countdown(private val from: Int) : Iterable<Int> {
override fun iterator() = object : Iterator<Int> {
private var current = from
override fun hasNext() = current > 0
override fun next() = current--
}
}
fun main() {
for (n in Countdown(3)) println(n) // 3 2 1
println(Countdown(5).filter { it % 2 == 1 }) // works with all collection helpers
}
S: Single Responsibility · L: Liskov Substitution
Traversal is separate from storage (S) and any Iterable can be used the same way (L).
In Android and Kotlin: Every for loop, Iterator, Sequence, Cursor from a database, Flow.
Run it: Iterator
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Null object ★ Popular
Instead of returning null and checking everywhere, return a harmless object that does nothing, so callers never need an if.
The idea in Kotlin
interface Logger { fun log(message: String) }
class ConsoleLogger : Logger { override fun log(message: String) = println(message) }
object NoLogger : Logger { override fun log(message: String) {} } // the null object
class Service(private val logger: Logger = NoLogger) {
fun work(): Int { logger.log("working"); return 42 } // no null checks
}
L: Liskov Substitution · D: Dependency Inversion
The do-nothing object is a valid substitute for the real one (L), so callers depend only on the interface (D).
In Android and Kotlin: Kotlin’s null safety covers many cases, but emptyList(), a no-op Logger or Analytics, and Collections.emptyIterator() are null objects.
Try it: Null object
Make NoDiscount return the price unchanged, and make DiscountBook.forUser return the user’s discount, or NoDiscount when there is none (never null). Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.
interface Discount { fun apply(price: Double): Double }
class Percent(private val pct: Double) : Discount {
override fun apply(price: Double): Double = price * (1 - pct / 100)
}
object NoDiscount : Discount {
override fun apply(price: Double): Double = TODO("return the price unchanged")
}
class DiscountBook(private val byUser: Map<String, Discount>) {
fun forUser(id: String): Discount = TODO("the user's discount, or NoDiscount")
}
Open in Kotlin Playground ↗
Hint
The elvis operator ?: gives you a fallback when the map lookup is null.
See the tests
fun main() {
val book = DiscountBook(mapOf("ana" to Percent(10.0)))
test("known user gets a discount") { check(same(book.forUser("ana").apply(100.0), 90.0)) { "wrong price" } }
test("unknown user pays full price") { check(same(book.forUser("zed").apply(100.0), 100.0)) { "wrong price" } }
test("no discount leaves the price alone") { check(same(NoDiscount.apply(55.5), 55.5)) { "wrong price" } }
}
Show a solution
object NoDiscount : Discount {
override fun apply(price: Double): Double = price
}
class DiscountBook(private val byUser: Map<String, Discount>) {
fun forUser(id: String): Discount = byUser[id] ?: NoDiscount
}
Where the solution shows only part of the code, replace the matching part of the starter.
Observer ★ Popular
An object (the subject) keeps a list of listeners and tells them whenever something changes, without knowing who they are. It is also known as publish/subscribe.
The idea in Kotlin
class Subject<T> {
private val listeners = mutableListOf<(T) -> Unit>()
fun subscribe(l: (T) -> Unit): () -> Unit { listeners.add(l); return { listeners.remove(l) } }
fun emit(value: T) = listeners.toList().forEach { it(value) }
}
val s = Subject<String>()
val stop = s.subscribe { println("got $it") }
s.emit("hello")
stop() // unsubscribe, so nothing leaks
D: Dependency Inversion · O: Open/Closed
The subject depends only on a listener type (D), and you add new listeners without changing the subject (O).
In Android and Kotlin: StateFlow, LiveData, Rx Observable, OnClickListener, broadcast receivers.
Try it: Observer
Write subscribe (it returns a function that unsubscribes) and emit. Forgetting to unsubscribe is a classic Android memory leak, so test that unsubscribing works. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.
class Subject<T> {
private val listeners = mutableListOf<(T) -> Unit>()
fun subscribe(listener: (T) -> Unit): () -> Unit = TODO("add it, and return a function that removes it")
fun emit(value: T) { TODO("tell every listener") }
}
Open in Kotlin Playground ↗
Hint
Loop over a copy (listeners.toList()) so a listener that unsubscribes during emit does not break the loop.
See the tests
fun main() {
test("every subscriber receives the value") {
val subject = Subject<String>()
val a = mutableListOf<String>()
val b = mutableListOf<String>()
subject.subscribe { a.add(it) }
subject.subscribe { b.add(it) }
subject.emit("hi")
check(a == listOf("hi") && b == listOf("hi")) { "a=$a b=$b" }
}
test("unsubscribe stops delivery") {
val subject = Subject<Int>()
val seen = mutableListOf<Int>()
val stop = subject.subscribe { seen.add(it) }
subject.emit(1)
stop()
subject.emit(2)
check(seen == listOf(1)) { "got $seen" }
}
test("a listener may unsubscribe while being notified") {
val subject = Subject<Int>()
var stop: () -> Unit = {}
stop = subject.subscribe { stop() }
subject.emit(1)
}
}
Show a solution
class Subject<T> {
private val listeners = mutableListOf<(T) -> Unit>()
fun subscribe(listener: (T) -> Unit): () -> Unit {
listeners.add(listener)
return { listeners.remove(listener) }
}
fun emit(value: T) { listeners.toList().forEach { it(value) } }
}
Where the solution shows only part of the code, replace the matching part of the starter.
State ★ Popular
Represent each state as its own object that knows what may happen next, instead of one big when over a status field.
The idea in Kotlin
sealed interface UiState {
object Loading : UiState
data class Content(val items: List<String>) : UiState
data class Error(val message: String) : UiState
}
// the view reacts to the current state; no scattered boolean flags like isLoading && !hasError
O: Open/Closed · S: Single Responsibility
Each state class has one job (S) and a new state is a new class (O). Illegal moves fail in one clear place.
In Android and Kotlin: Screen state in MVVM (Loading, Content, Error sealed types), media player states, order or download status.
Try it: State
Finish the order lifecycle. Created and Paid can move on and be cancelled; Shipped can only move to Delivered. Any other move must throw IllegalStateException (the interface already does this by default). Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.
sealed interface OrderState {
fun next(): OrderState = throw IllegalStateException("no next state from ${this::class.simpleName}")
fun cancel(): OrderState = throw IllegalStateException("cannot cancel from ${this::class.simpleName}")
}
object Created : OrderState {
override fun next(): OrderState = TODO("go to Paid")
override fun cancel(): OrderState = TODO("go to Cancelled")
}
object Paid : OrderState {
override fun next(): OrderState = TODO("go to Shipped")
override fun cancel(): OrderState = TODO("go to Cancelled")
}
object Shipped : OrderState {
override fun next(): OrderState = TODO("go to Delivered")
}
object Delivered : OrderState
object Cancelled : OrderState
Open in Kotlin Playground ↗
Hint
Return the next object. States that do not override a method inherit the default, which throws. That is exactly the behavior you want.
See the tests
fun main() {
test("happy path") {
check(Created.next() === Paid) { "Created -> Paid" }
check(Paid.next() === Shipped) { "Paid -> Shipped" }
check(Shipped.next() === Delivered) { "Shipped -> Delivered" }
}
test("can cancel before shipping") {
check(Created.cancel() === Cancelled) { "Created -> Cancelled" }
check(Paid.cancel() === Cancelled) { "Paid -> Cancelled" }
}
test("cannot cancel after shipping") {
val problem = runCatching { Shipped.cancel() }.exceptionOrNull()
check(problem is IllegalStateException) { "expected IllegalStateException, got $problem" }
}
test("delivered is final") {
val problem = runCatching { Delivered.next() }.exceptionOrNull()
check(problem is IllegalStateException) { "expected IllegalStateException, got $problem" }
}
}
Show a solution
object Created : OrderState {
override fun next(): OrderState = Paid
override fun cancel(): OrderState = Cancelled
}
object Paid : OrderState {
override fun next(): OrderState = Shipped
override fun cancel(): OrderState = Cancelled
}
object Shipped : OrderState {
override fun next(): OrderState = Delivered
}
Where the solution shows only part of the code, replace the matching part of the starter.
Strategy ★ Popular
Put each interchangeable algorithm behind one small interface and let the caller choose which one to use.
The idea in Kotlin
interface ShippingCost { fun cost(kg: Double): Double }
class Standard : ShippingCost { override fun cost(kg: Double) = 5.0 }
class Express : ShippingCost { override fun cost(kg: Double) = 10.0 + 2.0 * kg }
class Order(private var shipping: ShippingCost) {
fun shippingFor(kg: Double) = shipping.cost(kg)
}
O: Open/Closed · D: Dependency Inversion · I: Interface Segregation
Replaces a growing when with new classes: Open/Closed (O). The context depends on the interface (D), kept small (I).
In Android and Kotlin: DispatcherProvider / schedulers, sort orders, payment providers, Comparator.
Try it: Strategy
Implement Express (10 plus 2 per kilogram) and let an Order switch strategy at run time with switchTo. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.
interface ShippingCost { fun cost(kg: Double): Double }
class Standard : ShippingCost {
override fun cost(kg: Double): Double = 5.0
}
class Express : ShippingCost {
override fun cost(kg: Double): Double = TODO("10 plus 2 per kilogram")
}
class Order(private var shipping: ShippingCost) {
fun switchTo(newShipping: ShippingCost) { TODO("use the new strategy from now on") }
fun shippingFor(kg: Double): Double = shipping.cost(kg)
}
Open in Kotlin Playground ↗
Hint
switchTo only has to replace the stored strategy: shipping = newShipping.
See the tests
fun main() {
test("standard is flat") { check(same(Order(Standard()).shippingFor(3.0), 5.0)) { "wrong cost" } }
test("express depends on weight") { check(same(Express().cost(3.0), 16.0)) { "wrong cost" } }
test("order can switch strategy") {
val order = Order(Standard())
order.switchTo(Express())
check(same(order.shippingFor(3.0), 16.0)) { "still using the old strategy" }
}
}
Show a solution
class Express : ShippingCost {
override fun cost(kg: Double): Double = 10.0 + 2.0 * kg
}
class Order(private var shipping: ShippingCost) {
fun switchTo(newShipping: ShippingCost) { shipping = newShipping }
fun shippingFor(kg: Double): Double = shipping.cost(kg)
}
Where the solution shows only part of the code, replace the matching part of the starter.
Template method ★ Popular
A base class fixes the order of the steps of an algorithm; subclasses fill in some of the steps.
The idea in Kotlin
abstract class Game {
fun play() { start(); turn(); end() } // the template: fixed order
protected abstract fun start()
protected abstract fun turn()
protected open fun end() = println("game over")
}
class Chess : Game() {
override fun start() = println("set up board")
override fun turn() = println("move a piece")
}
O: Open/Closed · L: Liskov Substitution
The skeleton is closed for change and variations are added by subclassing (O); subclasses stay interchangeable (L).
In Android and Kotlin: Activity.onCreate/onStart/onResume (lifecycle skeleton), AsyncTask (old), RecyclerView.Adapter, test setUp/tearDown.
Try it: Template method
Write export() so it runs header(), body() and footer() in that order, joined with a newline. Do not change the subclasses. Open the starter in the Kotlin Playground, press Run, edit the code, and run again until every line says PASS.
abstract class DataExporter {
fun export(): String = TODO("header, body, footer joined with a newline")
protected abstract fun header(): String
protected abstract fun body(): String
protected open fun footer(): String = "-- end --"
}
class CsvExporter : DataExporter() {
override fun header() = "id,name"
override fun body() = "1,Ana"
}
class HtmlExporter : DataExporter() {
override fun header() = "<h1>Users</h1>"
override fun body() = "<p>Ana</p>"
override fun footer() = "<hr>"
}
Open in Kotlin Playground ↗
Hint
listOf(a, b, c).joinToString("\n") joins the three steps with a newline.
See the tests
fun main() {
test("csv uses the default footer") {
val out = CsvExporter().export()
check(out == "id,name\n1,Ana\n-- end --") { "got '$out'" }
}
test("html overrides the footer") {
val out = HtmlExporter().export()
check(out == "<h1>Users</h1>\n<p>Ana</p>\n<hr>") { "got '$out'" }
}
}
Show a solution
fun export(): String = listOf(header(), body(), footer()).joinToString("\n")
Where the solution shows only part of the code, replace the matching part of the starter.
Blackboard
Several independent experts read and write a shared workspace (the blackboard). Each acts when the data it needs appears, until the problem is solved.
The idea in Kotlin (a complete program you can run)
class Blackboard { val facts = mutableMapOf<String, Any>() }
interface Expert {
fun canContribute(b: Blackboard): Boolean
fun contribute(b: Blackboard)
}
class Tokenizer : Expert {
override fun canContribute(b: Blackboard) = "text" in b.facts && "words" !in b.facts
override fun contribute(b: Blackboard) { b.facts["words"] = (b.facts["text"] as String).split(" ") }
}
class Counter : Expert {
override fun canContribute(b: Blackboard) = "words" in b.facts && "count" !in b.facts
override fun contribute(b: Blackboard) { b.facts["count"] = (b.facts["words"] as List<*>).size }
}
fun main() {
val board = Blackboard().apply { facts["text"] = "design patterns in kotlin" }
val experts = listOf(Counter(), Tokenizer()) // order does not matter
while (true) {
val expert = experts.firstOrNull { it.canContribute(board) } ?: break
expert.contribute(board)
}
println(board.facts)
}
O: Open/Closed · S: Single Responsibility · D: Dependency Inversion
New experts plug in without changing the others (O); each has one skill (S).
In Android and Kotlin: Pipelines where steps react to shared state, speech and image recognition, shared build or sync context.
Run it: Blackboard
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Interpreter
Represent a small language as a tree of objects and give each node an evaluate step. Good for rules, formulas and filters.
The idea in Kotlin (a complete program you can run)
sealed interface Expr
data class Num(val value: Int) : Expr
data class Add(val left: Expr, val right: Expr) : Expr
data class Mul(val left: Expr, val right: Expr) : Expr
fun eval(e: Expr): Int = when (e) {
is Num -> e.value
is Add -> eval(e.left) + eval(e.right)
is Mul -> eval(e.left) * eval(e.right)
}
fun main() {
println(eval(Add(Num(2), Mul(Num(3), Num(4))))) // 2 + 3 * 4 = 14
}
A new kind of expression is a new class or branch (O).
In Android and Kotlin: Regex engines, SQL parsers, search filters, simple rule engines. A Kotlin sealed interface plus when is the compact form.
Run it: Interpreter
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Mediator
Objects do not talk to each other directly. They talk through a mediator that knows who should hear what, which untangles many-to-many wiring.
The idea in Kotlin (a complete program you can run)
interface Mediator { fun send(from: String, message: String) }
class User(val name: String) {
var room: Mediator? = null
fun say(message: String) { room?.send(name, message) }
fun receive(message: String) = println("$name got '$message'")
}
class ChatRoom : Mediator {
private val users = mutableMapOf<String, User>()
fun join(user: User) { users[user.name] = user; user.room = this }
override fun send(from: String, message: String) {
users.values.filter { it.name != from }.forEach { it.receive("$from: $message") }
}
}
fun main() {
val room = ChatRoom()
val ana = User("Ana"); val ben = User("Ben"); val cy = User("Cy")
listOf(ana, ben, cy).forEach { room.join(it) }
ana.say("hi everyone") // Ben and Cy hear it; nobody holds a reference to anyone else
}
S: Single Responsibility · D: Dependency Inversion
Coordination lives in one place (S), participants depend on the mediator interface (D).
In Android and Kotlin: A chat room, a form controller that enables Submit when fields are valid, Dialog/ViewModel coordinating screens, an event bus.
Run it: Mediator
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Memento
Capture an object’s state in a snapshot so you can restore it later (undo), without exposing its insides.
The idea in Kotlin (a complete program you can run)
class Editor {
var text = ""
class Memento(val text: String)
fun save() = Memento(text)
fun restore(m: Memento) { text = m.text }
}
fun main() {
val editor = Editor()
editor.text = "version 1"
val snapshot = editor.save()
editor.text = "version 2 (oops)"
println(editor.text)
editor.restore(snapshot)
println(editor.text) // version 1
}
The snapshot is its own small type (S); the owner decides what to save.
In Android and Kotlin: onSaveInstanceState and SavedStateHandle, undo stacks, game save points.
Run it: Memento
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Servant
Put behavior that many unrelated classes need into one helper class (the servant), instead of copying it into each of them.
The idea in Kotlin (a complete program you can run)
interface Movable { var x: Int; var y: Int }
class Sprite(override var x: Int, override var y: Int) : Movable
class Label(override var x: Int, override var y: Int) : Movable
object MoveServant {
fun moveBy(items: List<Movable>, dx: Int, dy: Int) {
items.forEach { it.x += dx; it.y += dy }
}
}
fun main() {
val sprite = Sprite(0, 0)
val label = Label(5, 5)
MoveServant.moveBy(listOf(sprite, label), 2, 3)
println("${sprite.x},${sprite.y} ${label.x},${label.y}")
}
S: Single Responsibility · D: Dependency Inversion
Shared behavior has one home (S); classes only need to satisfy a small interface (D).
In Android and Kotlin: Utility objects such as a Mover or Formatter that work on anything implementing a small interface.
Run it: Servant
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Specification
Express business rules as small objects that answer yes or no, then combine them with and, or, not.
The idea in Kotlin (a complete program you can run)
fun interface Spec<T> { fun isSatisfiedBy(item: T): Boolean }
infix fun <T> Spec<T>.and(other: Spec<T>) = Spec<T> { isSatisfiedBy(it) && other.isSatisfiedBy(it) }
infix fun <T> Spec<T>.or(other: Spec<T>) = Spec<T> { isSatisfiedBy(it) || other.isSatisfiedBy(it) }
operator fun <T> Spec<T>.not() = Spec<T> { !isSatisfiedBy(it) }
data class Product(val price: Int, val inStock: Boolean)
fun main() {
val cheap = Spec<Product> { it.price < 50 }
val available = Spec<Product> { it.inStock }
val deal = cheap and available
listOf(Product(20, true), Product(20, false), Product(90, true))
.forEach { println("$it -> ${deal.isSatisfiedBy(it)}") }
println((!cheap).isSatisfiedBy(Product(90, true)))
}
O: Open/Closed · S: Single Responsibility · D: Dependency Inversion
New rules are new specs (O), each with one condition (S), combined through one interface (D).
In Android and Kotlin: Search filters, validation rules, feature-flag conditions, Room/SQL query builders.
Run it: Specification
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Visitor
Add new operations to a family of classes without editing them, by letting a visitor object handle each class.
The idea in Kotlin (a complete program you can run)
sealed interface Shape
data class Circle(val r: Double) : Shape
data class Rect(val w: Double, val h: Double) : Shape
interface ShapeVisitor<R> {
fun visit(c: Circle): R
fun visit(r: Rect): R
}
fun <R> Shape.accept(v: ShapeVisitor<R>): R = when (this) {
is Circle -> v.visit(this)
is Rect -> v.visit(this)
}
class AreaVisitor : ShapeVisitor<Double> {
override fun visit(c: Circle) = Math.PI * c.r * c.r
override fun visit(r: Rect) = r.w * r.h
}
class NameVisitor : ShapeVisitor<String> {
override fun visit(c: Circle) = "circle"
override fun visit(r: Rect) = "rectangle"
}
fun main() {
val shapes = listOf(Circle(1.0), Rect(2.0, 3.0))
shapes.forEach { println("${it.accept(NameVisitor())}: ${it.accept(AreaVisitor())}") }
}
O: Open/Closed · S: Single Responsibility
New operations are new visitors (O); each visitor is one job (S).
In Android and Kotlin: In Kotlin a sealed hierarchy with when usually replaces it, but the classic form still appears in compilers, Kotlin symbol processing and AST tools.
Run it: Visitor
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗5. Concurrency patterns
How code behaves when many things run at once: threads, locks, queues and schedulers. All demos here use plain JVM classes so they run in the Kotlin Playground. In real Android apps, coroutines wrap most of these ideas.
Lock ★ Popular
Only one thread at a time may run a critical section; the others wait their turn. This prevents lost updates on shared data.
The idea in Kotlin (a complete program you can run)
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.thread
import kotlin.concurrent.withLock
class Account {
private val lock = ReentrantLock()
var balance = 0
private set
fun deposit(n: Int) = lock.withLock { balance += n }
}
fun main() {
val account = Account()
val threads = List(4) { thread { repeat(1000) { account.deposit(1) } } }
threads.forEach { it.join() }
println(account.balance) // always 4000; remove the lock and it may be less
}
Locking sits inside the class that owns the data (S), so callers cannot forget it.
In Android and Kotlin: synchronized, ReentrantLock and withLock { }, Mutex in coroutines.
Run it: Lock
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Scheduler ★ Popular
Decide when and on which thread tasks run: now, later, repeatedly, on a pool. Keep that choice in one replaceable place.
The idea in Kotlin (a complete program you can run)
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
fun main() {
val scheduler = Executors.newScheduledThreadPool(1)
val job = scheduler.scheduleAtFixedRate({ println("tick") }, 0, 20, TimeUnit.MILLISECONDS)
scheduler.schedule({ println("one-off, 30 ms later") }, 30, TimeUnit.MILLISECONDS)
Thread.sleep(70)
job.cancel(false)
scheduler.shutdown()
}
O: Open/Closed · D: Dependency Inversion
Code depends on a scheduler interface (D), so production uses real threads and tests use immediate ones. See AssessmentSchedulers in architecture Part 2.
In Android and Kotlin: Dispatchers.IO/Main, RxJava Schedulers, Handler.postDelayed, WorkManager, ScheduledExecutorService.
Run it: Scheduler
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Thread pool ★ Popular
Keep a fixed number of worker threads and feed them tasks from a queue, instead of creating a new thread for each task.
The idea in Kotlin (a complete program you can run)
import java.util.concurrent.Callable
import java.util.concurrent.Executors
fun main() {
val pool = Executors.newFixedThreadPool(2)
val futures = (1..6).map { n -> pool.submit(Callable { n * n to Thread.currentThread().name }) }
val results = futures.map { it.get() }
println(results.map { it.first }) // [1, 4, 9, 16, 25, 36]
println("threads used: ${results.map { it.second }.distinct().size} (at most 2)")
pool.shutdown()
}
S: Single Responsibility · D: Dependency Inversion
Thread management is separate from the tasks (S); tasks depend on an executor interface (D).
In Android and Kotlin: Executors.newFixedThreadPool, OkHttp’s dispatcher, Dispatchers.IO (a shared pool), AsyncTask’s old thread pool.
Run it: Thread pool
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Active object
An object owns its own thread and a queue. Callers send requests and get a future back; the object handles them one at a time, so its state needs no locks.
The idea in Kotlin (a complete program you can run)
import java.util.concurrent.Callable
import java.util.concurrent.Executors
import java.util.concurrent.Future
class Counter {
private val executor = Executors.newSingleThreadExecutor()
private var value = 0 // only ever touched by the executor thread
fun increment(): Future<Int> = executor.submit(Callable { ++value })
fun shutdown() = executor.shutdown()
}
fun main() {
val counter = Counter()
val results = (1..5).map { counter.increment() }
println(results.map { it.get() }) // [1, 2, 3, 4, 5], in order, no locks
counter.shutdown()
}
S: Single Responsibility · D: Dependency Inversion
The object’s state is owned by one thread (S); callers only see a small method interface (D).
In Android and Kotlin: A single-thread executor, a HandlerThread, a coroutine actor, the Android main thread itself.
Run it: Active object
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Balking
If an object is not in the right state for a request (already busy), the request is ignored instead of waiting.
The idea in Kotlin (a complete program you can run)
import java.util.concurrent.atomic.AtomicBoolean
class Saver {
private val busy = AtomicBoolean(false)
fun save(work: () -> String = { "saved" }): String {
if (!busy.compareAndSet(false, true)) return "balked: already saving"
try {
return work()
} finally {
busy.set(false)
}
}
}
fun main() {
val saver = Saver()
println(saver.save()) // saved
println(saver.save { saver.save() }) // the inner call arrives while busy, so it balks
}
The state check sits inside the object (S).
In Android and Kotlin: Ignoring a second tap while a save is running, debouncing a refresh, if (isLoading) return.
Run it: Balking
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Binding properties
Link two properties so that when one changes, the other follows automatically.
The idea in Kotlin (a complete program you can run)
class Property<T>(initial: T) {
private var value = initial
private val listeners = mutableListOf<(T) -> Unit>()
fun get() = value
fun set(newValue: T) {
if (newValue != value) { // only fire on a real change, so bindings cannot loop
value = newValue
listeners.forEach { it(newValue) }
}
}
fun bind(other: Property<T>) {
other.set(value)
listeners.add { other.set(it) }
other.listeners.add { set(it) }
}
}
fun main() {
val a = Property("x")
val b = Property("y")
a.bind(b)
println(b.get()) // x
a.set("hello")
println(b.get()) // hello
b.set("from b")
println(a.get()) // from b
}
O: Open/Closed · D: Dependency Inversion
Views and models stay in sync without knowing each other (D), and new bindings need no edits (O).
In Android and Kotlin: Jetpack Compose State, StateFlow collected into a UI, Data Binding two-way @={}, JavaFX properties.
Run it: Binding properties
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Double-checked locking
Check a condition without a lock first (fast path), and only lock and check again if work is really needed. Used to create a shared object once.
The idea in Kotlin (a complete program you can run)
class Config private constructor() {
companion object {
@Volatile private var instance: Config? = null
fun get(): Config =
instance ?: synchronized(this) { // 1st check without a lock
instance ?: Config().also { instance = it } // 2nd check inside the lock
}
}
}
fun main() {
println(Config.get() === Config.get()) // true
// In real code, prefer: val config by lazy { Config() }
}
Thread-safe creation is isolated in one place (S).
In Android and Kotlin: Kotlin hides it: by lazy uses it internally. Write it yourself only to learn why @Volatile matters.
Run it: Double-checked locking
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Event-based asynchronous
Start work in the background and get notified by an event (callback) when it finishes or fails, instead of waiting for it.
The idea in Kotlin (a complete program you can run)
import kotlin.concurrent.thread
fun fetchAsync(id: Int, onDone: (String) -> Unit, onError: (Throwable) -> Unit): Thread = thread {
try {
Thread.sleep(50) // pretend to call a server
onDone("user#$id")
} catch (e: Exception) {
onError(e)
}
}
fun main() {
val worker = fetchAsync(7, onDone = { println("done: $it") }, onError = { println("error: $it") })
println("main keeps going") // printed first
worker.join()
}
O: Open/Closed · D: Dependency Inversion
The caller depends on callback types, not on how the work is done (D).
In Android and Kotlin: Click and response listeners, Call.enqueue(callback) in Retrofit, broadcast receivers. Coroutines and Futures are the modern replacement.
Run it: Event-based asynchronous
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Guarded suspension
A thread waits until a condition is true (a message has arrived) before it continues.
The idea in Kotlin (a complete program you can run)
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.thread
import kotlin.concurrent.withLock
class Mailbox {
private val lock = ReentrantLock()
private val hasMessage = lock.newCondition()
private var message: String? = null
fun put(m: String) = lock.withLock {
message = m
hasMessage.signalAll()
}
fun take(): String = lock.withLock {
while (message == null) hasMessage.await() // the guard: wait until it is true
message!!.also { message = null }
}
}
fun main() {
val box = Mailbox()
val reader = thread { println("got: ${box.take()}") }
Thread.sleep(50) // the reader is waiting
box.put("hello")
reader.join()
}
Waiting rules are encapsulated in the mailbox (S).
In Android and Kotlin: BlockingQueue.take(), CountDownLatch, a coroutine channel.receive() (suspends instead of blocking).
Run it: Guarded suspension
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Join
Wait until several concurrent tasks have all finished before continuing.
The idea in Kotlin (a complete program you can run)
import kotlin.concurrent.thread
fun main() {
val results = java.util.Collections.synchronizedList(mutableListOf<Int>())
val workers = (1..3).map { n ->
thread {
Thread.sleep(10L * n) // different durations
results.add(n * n)
}
}
workers.forEach { it.join() } // wait for all of them
println(results.sorted()) // [1, 4, 9]
}
The waiting logic is in one place (S).
In Android and Kotlin: Thread.join(), CountDownLatch, awaitAll() on coroutine Deferreds, Tasks.whenAll, Rx zip.
Run it: Join
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Messaging design pattern (MDP)
Components communicate by sending messages into a queue instead of calling each other directly or sharing memory.
The idea in Kotlin (a complete program you can run)
import java.util.concurrent.LinkedBlockingQueue
import kotlin.concurrent.thread
sealed interface Msg
data class Greet(val name: String) : Msg
object Stop : Msg
fun main() {
val inbox = LinkedBlockingQueue<Msg>()
val consumer = thread {
while (true) {
val msg = inbox.take() // waits for the next message
if (msg is Greet) println("hello ${msg.name}") else break
}
}
inbox.put(Greet("Ana"))
inbox.put(Greet("Ben"))
inbox.put(Stop)
consumer.join()
}
O: Open/Closed · S: Single Responsibility · D: Dependency Inversion
Producers and consumers are decoupled (D); new message types are added without editing the sender (O).
In Android and Kotlin: Handler/Looper, Channel, BlockingQueue, WorkManager, Kafka and other brokers on the backend.
Run it: Messaging design pattern (MDP)
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Monitor object
An object whose methods are all mutually exclusive, so only one thread is inside it at a time. The object guards its own state.
The idea in Kotlin (a complete program you can run)
import kotlin.concurrent.thread
class BoundedCounter(private val max: Int) {
private var value = 0
@Synchronized fun increment(): Boolean {
if (value >= max) return false
value++
return true
}
@Synchronized fun current() = value
}
fun main() {
val counter = BoundedCounter(5)
val threads = List(4) { thread { repeat(10) { counter.increment() } } }
threads.forEach { it.join() }
println(counter.current()) // exactly 5, never more
}
The class protects its own invariants (S).
In Android and Kotlin: Java/Kotlin @Synchronized methods, Vector, StringBuffer.
Run it: Monitor object
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Reactor
One event loop waits for events and dispatches each to the handler registered for it.
The idea in Kotlin (a complete program you can run)
class Reactor {
private val handlers = mutableMapOf<String, (String) -> Unit>()
private val events = ArrayDeque<Pair<String, String>>()
fun register(type: String, handler: (String) -> Unit) { handlers[type] = handler }
fun post(type: String, payload: String) { events.addLast(type to payload) }
fun run() {
while (events.isNotEmpty()) {
val (type, payload) = events.removeFirst()
val handler = handlers[type]
if (handler != null) handler(payload) else println("no handler for $type")
}
}
}
fun main() {
val reactor = Reactor()
reactor.register("click") { println("clicked $it") }
reactor.register("key") { println("key $it") }
reactor.post("click", "OK")
reactor.post("key", "A")
reactor.post("scroll", "10")
reactor.run()
}
O: Open/Closed · D: Dependency Inversion
Handlers register for event types without the loop knowing them (O, D).
In Android and Kotlin: The Android main thread’s Looper + Handler, Netty and other event-loop servers, Node.js.
Run it: Reactor
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Read-write lock
Many threads may read at the same time, but a writer needs exclusive access. Faster than one plain lock when reads dominate.
The idea in Kotlin (a complete program you can run)
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
class Cache {
private val lock = ReentrantReadWriteLock()
private val map = mutableMapOf<String, String>()
fun get(key: String): String? = lock.read { map[key] } // many readers allowed
fun put(key: String, value: String) { lock.write { map[key] = value } } // exclusive
}
fun main() {
val cache = Cache()
cache.put("lang", "Kotlin")
println(cache.get("lang"))
}
Locking strategy is hidden inside the cache (S).
In Android and Kotlin: ReentrantReadWriteLock with Kotlin’s read { } and write { } helpers, in-memory caches.
Run it: Read-write lock
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗Thread-specific storage
Give each thread its own private copy of a value, so threads do not need locks to share it.
The idea in Kotlin (a complete program you can run)
import kotlin.concurrent.thread
val requestId = ThreadLocal<String>()
fun log(message: String) = println("[${requestId.get()}] $message")
fun main() {
val a = thread { requestId.set("req-1"); log("working") }
a.join()
val b = thread { requestId.set("req-2"); log("working") }
b.join() // each thread saw only its own value
}
Per-thread context is kept out of method signatures (S).
In Android and Kotlin: ThreadLocal, the current Looper (Looper.myLooper()), coroutine context elements.
Run it: Thread-specific storage
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗CPU atomic operation
Use a single indivisible hardware-supported step (like compare-and-set) so threads can update a value safely without locks.
The idea in Kotlin (a complete program you can run)
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.thread
fun main() {
val hits = AtomicInteger()
val threads = List(4) { thread { repeat(1000) { hits.incrementAndGet() } } }
threads.forEach { it.join() }
println(hits.get()) // always 4000
val flag = AtomicInteger(0)
println(flag.compareAndSet(0, 1)) // true: it was 0, now 1
println(flag.compareAndSet(0, 2)) // false: it is no longer 0
}
Simple shared counters stay simple (S) without a lock object.
In Android and Kotlin: AtomicInteger, AtomicReference, compareAndSet, kotlinx.atomicfu.
Run it: CPU atomic operation
Open the program in the Kotlin Playground, press Run, then change something (a value, a class, a rule) and run it again to see what happens.
Open in Kotlin Playground ↗6. Patterns and SOLID at a glance
A tick means the pattern is a good way to practice that principle. Column letters link to the matching section of the SOLID article.
Notice that D, O and S show up most. Depending on small interfaces, adding behavior by adding classes, and giving each class one job are the heart of almost every pattern.
7. When not to use a pattern
- Patterns solve pain, not boredom. If nothing hurts, the plain code is the right code (KISS).
- Wait for the second case. One payment method needs no Strategy. A second one is the signal.
- Kotlin already has many.
objectis a Singleton,by lazyis Lazy initialization,byis Delegation,copy()is Prototype,use { }is RAII, extension functions replace many wrappers,sealed+whenreplaces Visitor, and lambdas replace many one-method Strategy and Command classes. - Do not name-drop. Class names like
OrderFactoryManagerImplhide the idea. Name things by what they do. - Be wary of global state. Singleton and Multiton need a
reset()in tests only because shared state leaks between them. - Prefer high-level tools for concurrency. Learn the concurrency patterns to understand what coroutines,
Flowand executors do for you, then use those.
8. Where to go next
- Software design pattern (Wikipedia): the main source for this article, with the five patterns I skipped.
- SOLID principles, explained and practised: the rules these patterns follow.
- Android architecture series, Part 2: ViewProxy, GlideProxy, Scheduler and Repository patterns in real app code.
- Clean Code summary and Clean Architecture summary.
- Kotlin Playground to keep experimenting.