The Hidden Cost of Eager Collections
Kotlin’s collection functions — map, filter, flatMap — are a joy to use. But they share one characteristic that can quietly hurt your app: they’re eager. Each call processes the entire source and allocates a brand new intermediate list. Chain three or four of them on a list of 50,000 items, and you’ve just allocated four lists where you might only need the first ten results.
Kotlin’s Sequence type solves this with lazy evaluation — elements are computed one at a time, on demand, and only as far as the pipeline needs to go. And the sequence { } builder lets you generate sequences from scratch using a surprisingly expressive coroutine-powered DSL.

How the sequence Builder Works
The sequence { } function takes a suspending lambda. Inside it, you call yield(value) to emit one element, or yieldAll(iterable) to emit a whole collection. Execution pauses at each yield and resumes only when the consumer requests the next element. This is coroutine suspension doing actual work for you:
val naturals = sequence {
var n = 0
while (true) { // infinite — that's fine, because it's lazy!
yield(n++)
}
}
println(naturals.take(5).toList()) // [0, 1, 2, 3, 4]
// Only 5 elements were ever computed
The while (true) loop is fine precisely because the sequence is lazy — it won’t actually loop infinitely unless someone consumes infinitely many elements. take(5) stops consumption after five items, so the generator only runs five iterations total.
yield vs yieldAll
yield emits a single value. yieldAll emits every item from an Iterable, Iterator, or another Sequence — lazily in the case of another sequence. This lets you compose sequences cleanly:
val combined = sequence {
yieldAll(1..3) // from a range
yield(99) // single special value
yieldAll(listOf(10, 20)) // from a list
yieldAll(generateMoreData()) // from another Sequence — still lazy!
}
println(combined.toList()) // [1, 2, 3, 99, 10, 20, ...]
A Real-World Example: Recursive File Walking
One of the most practical uses for a custom sequence is recursive traversal of hierarchical data — like a file system or a tree of view nodes — where you want to iterate lazily and stop as soon as you find what you’re looking for.
import java.io.File
fun File.walkFilesLazily(): Sequence = sequence {
val stack = ArrayDeque()
stack.addLast(this@walkFilesLazily)
while (stack.isNotEmpty()) {
val current = stack.removeLast()
when {
current.isFile -> yield(current)
current.isDirectory -> current.listFiles()?.forEach { stack.addLast(it) }
}
}
}
// Find the first Kotlin file in a huge project tree — stops as soon as it's found
val firstKtFile = File("/projects/myapp")
.walkFilesLazily()
.firstOrNull { it.extension == "kt" }
If you’d used toList() first and then firstOrNull, you’d have walked the entire tree into memory. With the lazy sequence, you stop the moment you find your answer.

Generating Paginated Data
Another pattern that shines with sequence is paginated API calls. Instead of loading all pages upfront, you stream them on demand:
fun fetchAllUsers(): Sequence = sequence {
var page = 1
while (true) {
val response = api.getUsers(page = page, pageSize = 50) // blocking call
yieldAll(response.users)
if (!response.hasMore) break
page++
}
}
// Process only the first 100 users — fetches exactly 2 pages
fetchAllUsers()
.filter { it.isActive }
.take(100)
.forEach { processUser(it) }
Only the pages actually required to satisfy take(100) are fetched. If the first page already has 100 active users, you make a single API call. Note that the API call here is blocking — for suspend-friendly pagination in coroutines, you’d use Kotlin’s Flow instead, but for scripting, background workers, and non-suspending contexts, this pattern is extremely handy.
Performance: Sequence vs List Chains
Here’s a quick mental model for when sequences win:
- Use a Sequence when you chain multiple operations (filter + map + take) on a large or potentially infinite source. Each element goes through the whole pipeline before the next element starts — no intermediate lists.
- Use a List when your source is small (fewer than ~100 elements) or when you only apply one operation. The overhead of sequence machinery isn’t worth it at small scales.
- Use a Sequence any time you might short-circuit early with
firstOrNull,take,takeWhile, orfind. Eager collections do useless work for elements past the cut-off.
Don’t Forget: Sequences Are Single-Pass
One gotcha: most sequences (especially those backed by generators) can only be iterated once. If you call toList() or iterate twice, the second pass may return no elements or behave unexpectedly. If you need to iterate a generated sequence multiple times, either convert it to a list first or make sure your builder produces a fresh sequence each time it’s called.
val seq = sequence { yield(1); yield(2); yield(3) }
println(seq.toList()) // [1, 2, 3]
println(seq.toList()) // [1, 2, 3] — fine, because sequence {} re-runs the builder
// BUT:
val iter = seq.iterator()
iter.next() // 1
// Don't reuse 'iter' — you'll get elements starting from 2, not from 1
Summary
The sequence { } builder is one of Kotlin’s most underused features. It lets you express infinite or on-demand data sources, recursive traversals, and paginated feeds in an imperative style that reads clearly — while the runtime handles lazy evaluation automatically. If you’re chaining collection operations on large data sets, dealing with paginated APIs, or traversing hierarchical structures, reach for sequence before you reach for an eager list.
This post was written by a human with the help of Claude, an AI assistant by Anthropic.
