Transforming Streams
Map and filter flowing data.
Transforming Streams is a free Scala for Backend Engineering & Functional Programming lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Scala for Backend Engineering & Functional Programming learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Operators as Transformations
Akka Streams provides a rich set of operators on Source and Flow that mirror Scala's collection API but run asynchronously and respect backpressure.
Each operator returns a new blueprint, so transformations are composed declaratively before the stream ever runs.
map and filter
map applies a synchronous function to every element; filter drops elements that fail a predicate. These are the workhorses of element-wise transformation.
Both preserve ordering and propagate completion and failure downstream.
val flow =
Flow[Int]
.filter(_ % 2 == 0)
.map(n => n * n)mapConcat for One-to-Many
When one input should produce several outputs, use mapConcat. It takes a function returning an iterable and flattens the results into the stream.
Returning an empty collection effectively drops the element.
val explode: Flow[String, String, akka.NotUsed] =
Flow[String].mapConcat(line => line.split(",").toList)
val words = Source(List("a,b", "c,d,e"))
.via(explode)grouped and sliding
grouped(n) batches consecutive elements into a Seq of up to n items, useful for bulk database writes. sliding(n) emits overlapping windows.
Batching reduces per-element overhead in I/O-heavy pipelines.
val batches: Source[Seq[Int], akka.NotUsed] =
Source(1 to 1000).grouped(100)
val windows =
Source(1 to 10).sliding(3, step = 1)scan and fold
scan emits the running accumulator after each element, giving an evolving state stream. fold emits only the final accumulated value once upstream completes.
Use scan for live counters and fold for terminal aggregates.
val running =
Source(1 to 5).scan(0)(_ + _) // 0,1,3,6,10,15
val total =
Source(1 to 5).fold(0)(_ + _) // 15mapAsync for Async Work
mapAsync(parallelism) calls a function returning a Future and emits results in order, running up to parallelism futures concurrently.
Use it for asynchronous calls like database lookups or HTTP requests where ordering matters.
import scala.concurrent.Future
val enriched =
Flow[UserId]
.mapAsync(parallelism = 4)(id => lookup(id))
def lookup(id: UserId): Future[User] = ???mapAsyncUnordered
mapAsyncUnordered behaves like mapAsync but emits each result as soon as it completes, ignoring input order.
It can improve throughput when downstream does not care about ordering, since a slow future no longer blocks faster ones.
val fast =
Flow[UserId]
.mapAsyncUnordered(parallelism = 8)(id => lookup(id))Stateful Transformation with statefulMapConcat
For per-element transformations that need mutable local state, statefulMapConcat creates fresh state per materialization and returns an iterable of outputs.
It is the safe way to keep counters or buffers without sharing state across stream runs.
val withIndex: Flow[String, (Int, String), akka.NotUsed] =
Flow[String].statefulMapConcat { () =>
var i = 0
elem => { i += 1; List((i, elem)) }
}Time-Based Operators
Streams can transform on time as well as content. throttle caps the emission rate, groupedWithin batches by size or elapsed time, and takeWithin limits duration.
These are essential for rate-limiting external APIs.
import scala.concurrent.duration._
val limited =
Source(1 to 1000)
.throttle(10, 1.second)
.groupedWithin(100, 500.millis)Handling Errors in Transformations
A thrown exception inside an operator fails the whole stream by default. A supervision strategy can instead resume (drop the bad element) or restart the stage.
Attach the strategy with withAttributes on the Flow.
import akka.stream.{ActorAttributes, Supervision}
val safe =
Flow[String].map(_.toInt)
.withAttributes(
ActorAttributes.supervisionStrategy(_ => Supervision.Resume))Composing Flows
Small Flows compose into larger ones with via, producing a single reusable Flow. This keeps each transformation focused and independently testable.
The composed Flow has the input type of the first and the output type of the last.
val parse = Flow[String].map(_.toInt)
val square = Flow[Int].map(n => n * n)
val parseAndSquare: Flow[String, Int, akka.NotUsed] =
parse.via(square)Quick Check
Consider asynchronous transformations and their ordering guarantees.
Recap
You explored transformation operators: element-wise map/filter, one-to-many mapConcat, batching grouped, accumulation with scan and fold, and async work via mapAsync.
You also saw stateful transforms, time-based operators like throttle, supervision strategies for errors, and how Flows compose with via. Next: how backpressure keeps these stages safe.
Frequently asked questions
Is the “Transforming Streams” lesson free?
Yes — the full text of “Transforming Streams” is free to read here on the web, and the Scala for Backend Engineering & Functional Programming course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Scala for Backend Engineering & Functional Programming course, upgrade to CoddyKit PRO.
What will I learn in “Transforming Streams”?
Map and filter flowing data. You practise Scala for Backend Engineering & Functional Programming with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Scala for Backend Engineering & Functional Programming?
No prior experience is required. Scala for Backend Engineering & Functional Programming on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Transforming Streams” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Scala for Backend Engineering & Functional Programming lesson?
Yes. Every Scala for Backend Engineering & Functional Programming lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Source, Flow, and Sink
- Transforming Streams
- Backpressure
- Running a Pipeline