Definition
- Window assigner – Decides, for each incoming element, which window or windows it belongs to (tumbling, sliding, session, or global).
- Trigger – Decides when a window's computation fires and emits a result; returns CONTINUE, FIRE, PURGE, or FIRE_AND_PURGE.
- Evictor – Optional component that removes elements from the window buffer before or after the window function runs.
- Default triggers – Event-time windows default to
EventTimeTrigger(fires on watermark), processing-time toProcessingTimeTrigger, global window has no useful default. - Evictor cost – Using an evictor forces Flink to buffer every raw element, disabling incremental aggregation and increasing memory usage.
Think of the National Bank of India sorting transaction slips into labeled trays. The window assigner is the clerk who reads each slip's time and drops it into the correct minute-labeled tray. The trigger is the supervisor who decides the moment to say "process this tray now." The evictor is an optional helper who, just before the supervisor processes a tray, removes certain slips, for example keeping only the last ten so the computation reflects only recent activity.
Understanding the Concepts
What is a window assigner?
The window assigner decides which window or windows each incoming element belongs to. You set it with the .window(...) call. Different assigners produce different window shapes.
import org.apache.flink.streaming.api.windowing.assigners.*;
import org.apache.flink.streaming.api.windowing.time.Time;
// Tumbling: each element in exactly one 1-minute window
stream.keyBy(txn -> txn.getAccountId())
.window(TumblingEventTimeWindows.of(Time.minutes(1)));
// Sliding: each element in multiple overlapping windows
stream.keyBy(txn -> txn.getAccountId())
.window(SlidingEventTimeWindows.of(Time.minutes(1), Time.seconds(10)));
// Session: elements grouped by gap
stream.keyBy(txn -> txn.getAccountId())
.window(EventTimeSessionWindows.withGap(Time.minutes(5)));
// Global: all elements in one window, needs custom trigger
stream.keyBy(txn -> txn.getAccountId())
.window(GlobalWindows.create());
What is a trigger?
The trigger decides when the window function actually runs. It returns a TriggerResult: CONTINUE (do nothing), FIRE (emit but keep window), PURGE (clear window), or FIRE_AND_PURGE (emit then clear).
import org.apache.flink.streaming.api.windowing.triggers.*;
// EventTimeTrigger: fires when watermark passes window end (default)
stream.window(TumblingEventTimeWindows.of(Time.minutes(1)))
.trigger(EventTimeTrigger.create())
.sum("amount");
// CountTrigger: fires every N elements
stream.window(GlobalWindows.create())
.trigger(CountTrigger.of(100))
.sum("amount");
// ProcessingTimeTrigger: fires on wall clock
stream.window(TumblingProcessingTimeWindows.of(Time.minutes(1)))
.trigger(ProcessingTimeTrigger.create())
.sum("amount");
TriggerResult values: CONTINUE → do nothing, keep accumulating FIRE → run window function, keep window contents PURGE → clear window contents, do not emit FIRE_AND_PURGE → run window function, then clear contents
How do you build a custom trigger?
You can override the default trigger to fire early for speculative results and then finally on the watermark.
import org.apache.flink.streaming.api.windowing.triggers.Trigger;
import org.apache.flink.streaming.api.windowing.triggers.TriggerResult;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
public class EarlyFiringTrigger
extends Trigger<Transaction, TimeWindow> {
private final int earlyCount;
public EarlyFiringTrigger(int earlyCount) {
this.earlyCount = earlyCount;
}
@Override
public TriggerResult onElement(Transaction txn, long timestamp,
TimeWindow window, TriggerContext ctx) {
// Register event-time timer for final firing
ctx.registerEventTimeTimer(window.maxTimestamp());
// Count elements, fire early every earlyCount
long count = ctx.getPartitionedState(
new ValueStateDescriptor<>("count", Long.class, 0L)).value();
count++;
if (count >= earlyCount) {
ctx.getPartitionedState(
new ValueStateDescriptor<>("count", Long.class, 0L))
.update(0L);
return TriggerResult.FIRE; // speculative result
}
return TriggerResult.CONTINUE;
}
@Override
public TriggerResult onEventTime(long time, TimeWindow window,
TriggerContext ctx) {
return TriggerResult.FIRE_AND_PURGE; // final result
}
@Override
public TriggerResult onProcessingTime(long time, TimeWindow window,
TriggerContext ctx) {
return TriggerResult.CONTINUE;
}
@Override
public void clear(TimeWindow window, TriggerContext ctx) {}
}
What is an evictor?
The evictor is optional and removes elements from the window buffer before or after the window function runs. Built-in evictors include CountEvictor, TimeEvictor, and DeltaEvictor.
import org.apache.flink.streaming.api.windowing.evictors.*;
// CountEvictor: keep only the last 10 elements
stream.keyBy(txn -> txn.getAccountId())
.window(TumblingEventTimeWindows.of(Time.minutes(1)))
.evictor(CountEvictor.of(10))
.sum("amount");
// TimeEvictor: keep only elements within the last 30 seconds
stream.keyBy(txn -> txn.getAccountId())
.window(TumblingEventTimeWindows.of(Time.minutes(1)))
.evictor(TimeEvictor.of(Time.seconds(30)))
.sum("amount");
How do the three components interact?
The interaction has a strict order: for each element, the assigner selects its windows and adds it to each window's buffer, the trigger is consulted and may say fire, and if an evictor is present it prunes the buffer before and after the window function runs.
// Full composition: assigner + custom trigger + evictor
stream.keyBy(txn -> txn.getAccountId())
.window(TumblingEventTimeWindows.of(Time.minutes(1))) // assigner
.trigger(new EarlyFiringTrigger(50)) // fire every 50 elements + final
.evictor(CountEvictor.of(10)) // keep only last 10
.apply(windowFunction);
// Execution order:
// 1. Assigner: event lands in [10:00, 10:01) window
// 2. Trigger: consulted -- CONTINUE or FIRE?
// 3. If FIRE and evictor present:
// a. Evictor.evictBefore: prune buffer
// b. WindowFunction.apply: compute over survivors
// c. Evictor.evictAfter: prune again (rarely used)
Why does using an evictor disable incremental aggregation?
Attaching an evictor forces Flink to buffer every raw element in the window rather than incrementally aggregating, because the evictor needs access to individual elements to decide which to remove. This disables the memory-efficient incremental path.
// WITHOUT evictor: incremental aggregation
stream.window(TumblingEventTimeWindows.of(Time.minutes(1)))
.reduce((a, b) -> merge(a, b)); // only one accumulated value in memory
// WITH evictor: full buffer
stream.window(TumblingEventTimeWindows.of(Time.minutes(1)))
.evictor(CountEvictor.of(10))
.reduce((a, b) -> merge(a, b)); // ALL elements buffered in memory!
// Memory usage: O(window_size) instead of O(1)
What are common mistakes?
Using an evictor without understanding the memory cost is the most common mistake. Another is forgetting to set a trigger on a global window, which means it never fires.
// WRONG -- global window without trigger, never fires
stream.window(GlobalWindows.create())
.sum("amount"); // Never emits! No trigger defined.
// CORRECT -- global window with explicit trigger
stream.window(GlobalWindows.create())
.trigger(CountTrigger.of(100))
.sum("amount"); // Fires every 100 elements
What should a senior engineer know?
Only reach for an evictor when you genuinely need element-level pruning, because it costs memory. Otherwise prefer a plain reduce or aggregate which Flink can run incrementally. A custom trigger gives speculative early results and a final correct one, which is the standard pattern for real-time dashboards that want both speed and eventual correctness.
Visual Overview
Window Component Interaction Order
Event arrives
│
▼
┌─────────────┐
│ Assigner │ Decides which window(s) the event belongs to
│ Tumbling / │
│ Sliding / │
│ Session │
└──────┬──────┘
│ event added to window buffer
▼
┌─────────────┐
│ Trigger │ Should the window fire now?
│ CONTINUE │ → wait for more events
│ FIRE │ → proceed to window function
│ PURGE │ → clear window
│ FIRE+PURGE │ → emit then clear
└──────┬──────┘
│ if FIRE:
▼
┌─────────────┐
│ Evictor │ (optional) prune buffer
│ evictBefore│ → remove elements before function
└──────┬──────┘
│
▼
┌─────────────┐
│ Window │ Compute aggregation over surviving elements
│ Function │
└──────┬──────┘
│
▼
┌─────────────┐
│ Evictor │ (optional) prune after function
│ evictAfter │
└─────────────┘
Key Takeaway
A Flink window is built from three cooperating components that interact in a strict order. The window assigner decides, for every incoming element, which window or windows it belongs to. A tumbling assigner puts an element into exactly one window, a sliding assigner can put one element into several overlapping windows, a session assigner groups bursts, and the global assigner puts everything into a single window.
The trigger decides when a window fires. Event-time windows default to firing when the watermark passes the window end, processing-time windows fire on the wall clock, and global windows need an explicit trigger like CountTrigger. A trigger returns CONTINUE, FIRE, PURGE, or FIRE_AND_PURGE, and you can build custom triggers that fire early for speculative results.
The evictor is optional and runs around the window function, removing elements from the buffer before or after computation. The crucial point is that attaching an evictor forces Flink to retain every raw element instead of aggregating incrementally, so it disables the memory-efficient path. Only reach for an evictor when you truly need element-level pruning.
At the National Bank of India Rajesh used a one-minute tumbling window with a custom trigger that fired every 50 transactions for speculative dashboard updates, and a count evictor that kept only the last 10 for a rolling risk average. He justified the evictor's memory cost because the use case genuinely required element-level access.
Remember one sentence: The assigner routes events to windows, the trigger decides when to fire, and the optional evictor prunes elements, but attaching an evictor disables incremental aggregation and increases memory, so use it only when needed.
---