How a stream pipeline executes
How Java stream pipelines execute: laziness, vertical traversal, fused operations, ReferencePipeline stages, Sink chains, stateful barriers, short-circuiting and stream flags.
On this page
⚙️ What does it mean for a stream pipeline to execute?
Stream execution is the moment a terminal operation starts pulling elements from the source through the pipeline. Before that, intermediate operations such as filter, map, peek, sorted and limit mostly build a chain of stage objects. It acts like connecting hoses before opening the tap: the path is assembled first, then water flows through it.
For this lesson, we'll focus on what happens between orders.stream().filter(...).map(...).toList() and the result: laziness, vertical traversal, operation fusion, the ReferencePipeline stage chain, the Sink chain built at terminal time, stateful barriers, short-circuiting and stream flags. Interviewers ask this because it exposes whether you learned streams as syntax or as a pull-based execution model.
Below the line (out of scope)
- Basic stream sources and creation APIs: Introduction to Java streams
- Operation categories in more detail: Intermediate vs terminal operations
- Short-circuiting choices like
findFirstandfindAny: findFirst vs findAny and short-circuiting - Parallel execution and spliterators: Parallel streams: when they help and hurt and Spliterator explained
The 30-second answer
A stream pipeline is lazy: intermediate operations create stages, and nothing traverses the source until a terminal operation runs. In a sequential pipeline, Java usually processes vertically, one element goes through filter, then map, then the terminal sink before the next element starts, so stateless operations are fused into one pass. Internally, a stream is an AbstractPipeline chain, with a ReferencePipeline.Head for the source and StatelessOp or StatefulOp stages for operations. When the terminal operation begins, the JDK builds a Sink chain backwards with wrapSink, then drives the source spliterator into the first sink. Stateful operations like sorted and distinct may buffer as barriers; short-circuiting operations like limit, findFirst and anyMatch can stop early. Flags such as SIZED, SORTED and DISTINCT let the implementation skip work, including count() skipping traversal for sized sources since Java 9.
What you’ll learn
- Show that intermediate operations are lazy and do nothing until a terminal operation starts traversal
- Explain vertical element-by-element processing and operation fusion in a single sequential pass
- Describe the internal
ReferencePipelinestage chain:Head,StatelessOpandStatefulOp - Draw the terminal-time
Sinkchain built backwards withwrapSink - Separate stateless operations from stateful barriers such as
sortedanddistinct - Explain short-circuiting operations and stream flags like
SIZED,SORTEDandDISTINCT, including thecount()andpeek()surprise since Java 9
01Picture it first: an assembly line that pulls
Picture three workstations connected by rollers: quality check, label printing and packing. A worker at the packing station asks for the next box. That request pulls one box through quality check, then label printing, then packing.
That is the stream model for stateless sequential pipelines. The terminal operation pulls; the source does not push a completed list through each stage.
A terminal operation pulls each order vertically through filter and map stages, one element at a time, rather than running one full horizontal pass per operation
Notice the direction. We often read the code left to right, but execution is demand driven from the terminal operation. That distinction explains most stream surprises.
02Laziness under the hood: nothing runs until terminal
Try it yourself first
Before reading the output, decide what prints before the count() line and what prints after it starts.
import java.util.List;
final class LazyPipelineDemo {
public static void main(String[] args) {
var pipeline = List.of("paid", "failed", "paid").stream()
.peek(s -> System.out.println("peek " + s))
.filter("paid"::equals);
System.out.println("pipeline built");
long count = pipeline.count();
System.out.println("count " + count);
}
}
The output is:
pipeline built
peek paid
peek failed
peek paid
count 2
peek did not run when the pipeline variable was assigned. It ran only when count() started traversing the source. This is why peek is a debugging tool, not a reliable place for business side effects.
The next example shows vertical processing and fusion. Java does not normally filter the whole list into a temporary list and then map the whole temporary list.
Try it yourself first
Read the code and predict the print order. If you expect all filters first and all maps second, this is the lesson's turning point.
import java.util.List;
final class VerticalExecutionDemo {
public static void main(String[] args) {
List.of("ann", "bob", "carla").stream()
.filter(name -> {
System.out.println("filter " + name);
return name.length() > 3;
})
.map(name -> {
System.out.println("map " + name);
return name.toUpperCase();
})
.forEach(name -> System.out.println("forEach " + name));
}
}
The output is:
filter ann
filter bob
filter carla
map carla
forEach CARLA
ann and bob never reach map. carla passes filter, then immediately goes to map, then immediately to forEach. That is operation fusion: the stateless stages are combined into one traversal.
03The internal model: pipeline stages and sinks
The public API hides the classes, but the shape is not mysterious. A reference stream starts with a ReferencePipeline.Head. Each intermediate operation appends another AbstractPipeline stage. Stateless operations such as filter, map and peek create ReferencePipeline.StatelessOp; stateful operations such as sorted and distinct create ReferencePipeline.StatefulOp.
At terminal time, the JDK builds a chain of Sink objects. A sink is like a tiny receiver with begin, accept, cancellationRequested and end. The terminal operation owns the last sink, and each intermediate stage wraps the downstream sink by calling wrapSink.
A ReferencePipeline stage chain is assembled left to right, then terminal evaluation builds a Sink chain backwards so source elements flow through filter, map and the terminal sink
Here is the important interaction. The stage chain is built as you call methods. The sink chain is built when the terminal operation evaluates. Then the source Spliterator feeds elements into the outermost sink, which calls the next sink, and so on.
For a pipeline like:
orders.stream()
.filter(Order::paid)
.map(Order::id)
.toList();
The simplified mental model is:
source spliterator
-> filter sink: if paid, downstream.accept(order)
-> map sink: downstream.accept(order.id())
-> terminal sink: add id to result list
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- Advanced: stateful barriers, short-circuiting and flags
- How to say it in the interview
From ₹199 · one-time payment · No auto-renewal