Skip to content
teach

Learning: Kotlin

Become the engineer trusted to own Kotlin on a team, in a backend service or an Android app: able to model a domain idiomatically, write and reason about coroutines and Flow, ship a typed, tested Kotlin service or Android component, and review someone's Kotlin and name concretely what a construct is costing them.

Latest lesson: 44. Testing Coroutines Beyond runTest

Success looks like

  • Model a domain with data classes, sealed classes and enums, and know when null safety, not an exception, is the correct signal.
  • Write and reason about coroutines and Flow, and compare the model to Java virtual threads.
  • Ship a typed, tested Kotlin backend service or Android component, designed idiomatically.
  • Given Kotlin written with a habit that merely compiles (defensive null checks instead of the type system, a callback instead of a coroutine), name the habit and rewrite it idiomatically.

Constraints

  • Assumes no prior Kotlin. Experience in Java or another JVM language shortens the early stages but is not required, and it brings habits Kotlin punishes quietly: defensive null checks instead of the type system, a class where a data class or sealed type would say it better.
  • Covers both backend/server-side and Android contexts, touching each where they diverge.
  • Needs only a JDK, the Kotlin toolchain and a terminal; Android Studio only where Android-specific material needs it.

Out of scope

  • JVM internals past what explains Kotlin's own compiled behaviour (full garbage-collector tuning, profiling, bytecode engineering): see programming/java, linked to for further depth rather than retaught.
  • Other JVM languages (Java, Scala, Clojure, Groovy) as subjects in their own right, though a contrast appears where it clarifies a Kotlin idiom.
  • Kotlin Multiplatform: targeting JS, Native, or Wasm, structuring expect/actual declarations across source sets, and the platform-specific stdlib differences those targets introduce. This arc is JVM-only throughout, per the mission's own framing; a lesson here would need its own Gradle setup and its own set of idioms, not a JVM lesson with a multiplatform footnote. kotlinx.serialization (lesson 38) is the one place multiplatform reach is mentioned, as a documented reason for a JVM-side design choice, not as multiplatform material in its own right.

The arc

Eleven stages, zero to senior. Not a lesson list: a stage takes several lessons, and the boundaries are soft.

Stage Lessons Covers Done when
1. Foundations 0001 to 0006 Null safety, val/var and immutability, values vs references, basic types and string templates, collections basics (List/Set/Map, mutable vs read-only), control flow (when, ranges, for) Can predict nullability and mutability without running the code
2. Modelling 0007 to 0012 Classes and properties, data classes, sealed classes and exhaustive when, enums, object declarations and companion objects, interfaces with default methods Models a domain without reaching for a class-per-thing hierarchy first
3. Idiom 0013 to 0018 Extension functions, scope functions (let/run/with/apply/also), higher-order functions and lambdas, inline functions and reified generics, delegation (by), operator overloading Writes Kotlin a reviewer would not describe as translated Java
4. Collections and sequences 0019 to 0021 Kotlin's collection operators, lazy Sequence vs eager collections, grouping and folding Chooses between a collection pipeline and a sequence and can defend the cost of each
5. Concurrency 0022 to 0027 Threads and the JVM memory model basics, suspend functions and coroutine builders, structured concurrency, coroutine context and dispatchers, Flow, cancellation and exception handling Can predict what a concurrent coroutine program does before running it, and compare the model to Java virtual threads
6. Testing and build 0028 to 0030 Kotlin test frameworks, mocking, the Gradle Kotlin DSL and dependency management Someone else can clone, build, test and run it
7. Shipping a service 0031 to 0033 Structuring a typed, tested backend service, Android-specific idioms where the platform diverges, generics and variance (in/out) Has structured a typed, tested Kotlin backend service or Android component; stage 10 completes it with an HTTP layer, configuration, logging and persistence
8. Judgment 0034 to 0035 Java interop, reviewing Kotlin and naming precisely what a construct is costing Trusted to make the call and explain it to someone else
9. Advanced Idiom 0036 to 0039 Type-safe builders and DSLs, value class/@JvmInline, kotlinx.serialization, Result and error-handling idioms Writes a small type-safe builder DSL, a zero-cost wrapper type, and a Result-based error path idiomatically
10. Completing the Service 0040 to 0043 An HTTP layer, configuration, logging, and persistence for the Kotlin backend service stage 7 started structuring Ships a Kotlin backend service with a routed HTTP layer, externalized configuration, structured logging, and database access, not only a structured shell
11. Testing Coroutines Beyond runTest 0044 Turbine's cancelAndIgnoreRemainingEvents() for a flow that never completes, and what a cancellation test actually has to assert given that cancellation is cooperative Tests an infinite flow without hanging, and tests cancellation by its actual, documented evidence rather than a Job flag alone

Lessons

Work through these in order.

# Lesson Teaches
0001 Null Safety Moving "could this be null" from a runtime surprise to a compile-time question, and how the Java habit undoes it
0002 val/var and Immutability Why val is the default worth defending, what it actually guarantees (and doesn't), and the Java habit of reaching for var out of reflex
0003 Values vs References Why == means something different in Kotlin than in Java, and the specific bug the Java habit of writing == produces
0004 Basic Types and String Templates Why Kotlin has no primitives at the language level, and string templates as a language feature rather than concatenation with extra syntax
0005 Collections Basics List, Set, and Map as read-only-by-interface collections, and why a mutable collection held by a val is still mutable
0006 Control Flow if and when as expressions that produce values, what a range actually is, and the stage 1 capstone of predicting nullability and mutability without running code
0007 Classes and Properties Properties as the replacement for Java's getter/setter boilerplate, and why the Kotlin docs themselves say to reach for a class last
0008 Data Classes What data class actually generates, and the subtle rule that only primary-constructor properties participate in any of it
0009 Sealed Classes and Exhaustive when How sealing a hierarchy turns a when's exhaustiveness from a manual promise into a compiler-checked guarantee
0010 Enums Each enum constant as a real object with its own state and behavior, and precisely where an enum fits over a sealed class
0011 Object Declarations and Companion Objects Singletons declared, not implemented by hand, and companion objects as Kotlin's actual replacement for Java's static members
0012 Interfaces with Default Methods Why a Kotlin interface can implement behavior but never hold state, and how to resolve a diamond conflict explicitly
0013 Extension Functions Adding behavior to a type without inheritance, and the static-dispatch gotcha that trips people up the first time they hit it
0014 Scope Functions Choosing among let, run, with, apply, and also by what each one returns and how it exposes the object, not by habit
0015 Higher-Order Functions and Lambdas Function types as real types, trailing-lambda syntax, and the receiver mechanism scope functions are actually built on
0016 Inline Functions and Reified Generics Why inline exists to remove a real cost, why it enables non-local returns, and why reified type parameters only work because of inlining
0017 Delegation Implementing an interface by forwarding to a held object with zero boilerplate, and reusable property behavior via by lazy and custom delegates
0018 Operator Overloading Operators as ordinary named functions with a fixed symbol mapping, and the actual discipline of implementing one only where it means what the symbol already means
0019 Collection Operators The collection operators as extension functions that return a new collection eagerly, and the Java Stream habit that misprices a chain of them
0020 Sequences and Laziness Why the same operator chain behaves differently on a Sequence, what a terminal operation is actually for, and when laziness costs more than it saves
0021 Grouping and Folding Aggregating with fold and reduce, grouping without building the groups, and the stage 4 capstone of defending what a pipeline costs
0022 Threads and the Memory Model Why Kotlin has no memory model of its own on the JVM, the three constructs it spells as annotations instead of keywords, and the two guarantees a val does not give you
0023 Suspend Functions and Coroutine Builders What suspend actually changes about a function, the three builders and what each one returns, and why suspending code still reads top to bottom
0024 Structured Concurrency The parent and child tree that makes cancellation predictable, why a scope you own is a scope you must cancel, and what GlobalScope actually costs
0025 Coroutine Context and Dispatchers The context as a set of elements combined with plus, which dispatcher belongs to which kind of work, and the one context element that silently breaks structured concurrency
0026 Flows A flow as the asynchronous third option after a list and a sequence, what cold actually means for each collector, and the context rule a flow builder enforces at runtime
0027 Cancellation and Exception Handling Why cancellation is cooperative and what makes code ignore it, why a CancellationException is not a failure, and how the coroutine model compares with Java virtual threads
0028 Test Frameworks The layers a Kotlin test actually sits on, why runTest's virtual time stops at the dispatcher boundary, and what that forces on the design of the code under test
0029 Mocking Why final-by-default changes what mocking means in Kotlin, what MockK does for the constructs that are not methods on an object, and when finality is telling you a seam is missing
0030 Gradle and Dependency Management The Kotlin DSL's type-safe accessors and where they run out, why a Gradle configuration is not a Maven scope, and the resolution rule that decides which version reaches the classpath
0031 Structuring a Service Where Kotlin files actually go and what goes inside a class, and the four decisions from earlier stages that decide whether a service is testable
0032 Android Divergences The scopes Android owns on your behalf, main-safety as a contract the callee keeps, and why composition lifetime is not lifecycle lifetime
0033 Generics and Variance Declaration-site variance in place of wildcards, what out and in actually promise about a type parameter, and how to read a projected signature
0034 Java Interop Why the boundary is asymmetric, what a platform type costs you and how to pay it back, and which annotations you owe a Java caller
0035 Reviewing Kotlin Turning the arc into a review instrument: the cost each habit hides, the shape of a comment that names it, and what not to review
0036 Type-Safe Builders and DSLs Lesson 15 taught the receiver-style function type that lets run and apply expose this; a type-safe builder is that exact mechanism nested recursively, and @DslMarker is the one attribute that keeps a nested builder from silently reaching the wrong receiver
0037 value class and @JvmInline A value class is the fix for primitive obsession that the JVM usually lets you have for free, represented as its own underlying value with no wrapper object at all, except in the specific, documented cases where the compiler still has to box it
0038 kotlinx.serialization A Java habit reaches for a library that inspects a class's shape through reflection at runtime; kotlinx.serialization generates that same logic at compile time instead, through a compiler plugin, which is exactly what lets it work on Kotlin targets that have no runtime reflection API at all
0039 Result and Error-Handling Idioms runCatching wraps any Throwable into a Result, which is exactly the problem inside a coroutine, since lesson 27's cooperative cancellation depends on CancellationException propagating uncaught, and runCatching swallows it into an ordinary failure unless a caller explicitly rethrows it first
0040 HTTP Layer Lesson 31 deliberately deferred routing to whichever framework you chose; Ktor's own routing block turns out to be lesson 36's type-safe builder DSL applied to HTTP, nothing installed by default, and request-parameter extraction that fails loudly rather than silently binding the wrong thing
0041 Configuration A service's actual behavior, its port, its database URL, its secrets, has to change across environments without recompiling a single line, and Ktor's configuration file plus one specific, easy-to-miss substitution idiom is how a default and an environment override live in exactly one place
0042 Logging SLF4J silently does nothing until a real logging backend is added as a dependency, and CallLogging's per-request MDC values are silently invisible until the log pattern itself is updated to print them, the same shape of trap appearing twice in the same lesson
0043 Persistence Exposed's DSL is another type-safe builder, and it is mostly blocking underneath, since it wraps JDBC; the stage 10 capstone is dispatching that blocking work correctly instead of stalling whatever thread happened to be running the request, and revisiting what stage 7's shipped service actually needed all along
0044 Testing Coroutines Beyond runTest Lesson 28 taught Turbine's awaitItem and the hang it causes if nothing terminates it; this lesson covers the specific tool for a flow that never terminates on its own, and what a cancellation test actually has to assert, given that cancellation is cooperative and does not update everything the instant cancel() is called

Reference

  • Glossary: canonical terms for this topic
  • Resources: trusted sources
  • Null Safety and Mutability: null safety, val/var, values vs references, predicting nullability and mutability
  • Modelling: data classes, sealed classes and exhaustive when, object declarations and companion objects, interfaces with default methods
  • Idiom: extension functions, scope functions (let/run/with/apply/also), delegation (by), operator overloading
  • Collections and Sequences: Kotlin's collection operators, lazy Sequence vs eager collections, grouping and folding
  • Concurrency: suspend functions and coroutine builders, structured concurrency, coroutine context and dispatchers, Flow, comparison to Java virtual threads
  • Testing and Build: Kotlin test frameworks, mocking, the Gradle Kotlin DSL and dependency management
  • Shipping a Service: structuring a typed, tested backend, Android-specific idioms, generics and variance
  • Judgment: Java interop, reviewing Kotlin and naming precisely what a construct is costing

How this works

Each lesson is short and self-contained. Answer keys are collapsed: recall first, then open them. The real-world reps matter more than the reading, and spacing them out is the point. Anything still unclear at the end of a lesson is worth chasing to its primary source before moving on.

Table of contents