Concurrent execution and emission order are separate contracts. The important difference between flow.async and mapParallel is output ordering, not simply speed.
Decision table
Section titled “Decision table”| Requirement | Choice | Output order | Pressure control |
|---|---|---|---|
| simple sequential transform | standard map | input | one at a time |
| overlap computation but retain response order | Flow.async | input | collect buffer |
| process whichever result finishes first | mapParallel(n) | completion may win | bounded by n |
Ordered concurrency
Section titled “Ordered concurrency”Flow.async converts each input into a LazyDeferred and starts it in the collect scope. Work overlaps, but downstream awaits deferred values in original order.
productIds.asFlow() .async(Dispatchers.IO) { id -> catalog.load(id) } .collect(capacity = 16) { product -> render(product) }A slow earlier item delays emission of later completed items. This head-of-line cost is appropriate when response order is contractual.
Capacity accepts Channel.BUFFERED, Channel.CONFLATED, or a non-negative number. It bounds result buffering; it is not permission for unbounded work.
Throughput-first transforms
Section titled “Throughput-first transforms”mapParallel coerces parallelism to at least one. One uses plain map; values above one use flatMapMerge(concurrency).
val stored = events.asFlow() .mapParallel(parallelism = 8, context = Dispatchers.IO) { event -> repository.persist(event) } .toList()Faster items may emit first, so input order must not be a public contract. This is a good fit for independent persistence, enrichment, and thumbnail work.
Sizing parallelism
Section titled “Sizing parallelism”Do not derive it from CPU count alone.
effective parallelism = min( application budget, connection pool capacity, remote concurrency limit, memory/buffer budget)With retries, worst-case in-flight work approaches parallelism × attempts. Sum competing pipelines that share the same downstream boundary.
Failure and cancellation
Section titled “Failure and cancellation”- transform failure fails collection and structured siblings are cancelled;
- collector cancellation propagates upstream and into active children;
flowOn(context)changes execution context, not lifecycle ownership;- remote work surviving timeout usually indicates a client cancellation-bridge problem.
What to test
Section titled “What to test”- parallelism one, zero, and negative values take the ordered sequential path;
- values above one are allowed to change output order;
- active transforms never exceed the configured bound;
- collector cancellation cleans up child work and remote requests;
- a slow first item demonstrates head-of-line waiting on the ordered path.
Operational signals
Section titled “Operational signals”Track P95/P99 item latency, active transforms, buffer utilization, and downstream waiting together. A permanently full buffer is a capacity mismatch. Locate that boundary before increasing parallelism.
Source and representative tests
Section titled “Source and representative tests”For callback and hot-stream delivery semantics, continue with Subjects and event contracts.