1. Introduction

Dataflow programming lets you express dependencies between concurrent computations declaratively. Values are bound once and read many times — the runtime handles ordering automatically. You never need to think about which thread runs first.

Groovy’s dataflow support integrates natively with async/await and the Awaitable API.

2. DataflowVariable

A DataflowVariable is a single-assignment variable. It starts unbound, can be written to exactly once, and any thread that reads it before binding will block until a value is available.

import groovy.concurrent.DataflowVariable

def x = new DataflowVariable()
def y = new DataflowVariable()
def z = new DataflowVariable()

async {
    z << await(x) + await(y)    // blocks until x and y are bound
}

async { x << 10 }              // bind x — order doesn't matter
async { y << 5 }               // bind y

println "Result: ${await(z)}"  // 15

Key properties:

  • Write-once: calling bind() or << a second time throws IllegalStateException

  • Implements Awaitable: works with await, combinators (Awaitable.all), and then chaining

  • Thread-safe: safe to bind from one thread and read from any number of others

2.1. Error binding

def v = new DataflowVariable()
v.bindError(new RuntimeException('computation failed'))
// Any thread that awaits v will receive the exception

2.2. Chaining with then

def v = new DataflowVariable()
def doubled = v.then { it * 2 }

async { v << 21 }
assert await(doubled) == 42

2.3. Combining multiple variables

def a = new DataflowVariable()
def b = new DataflowVariable()
def c = new DataflowVariable()

async { a << 1 }
async { b << 2 }
async { c << 3 }

def results = await(a, b, c)
assert results == [1, 2, 3]

3. Dataflows

Dataflows is a convenience class that auto-creates DataflowVariable instances on demand. Property reads block until bound; property writes bind the variable.

import groovy.concurrent.Dataflows

def df = new Dataflows()

async { df.z = df.x + df.y }   // blocks on x and y, binds z
async { df.x = 10 }
async { df.y = 5 }

println "Result: ${df.z}"      // 15

This is the most concise way to write dataflow programs in Groovy. Variables are created lazily — no declarations needed.

def df = new Dataflows()

async { df.fullName = "${df.first} ${df.last}" }
async { df.first = 'John' }
async { df.last = 'Doe' }

assert df.fullName == 'John Doe'

3.1. Accessing the underlying variable

Use getVariable() when you need the DataflowVariable itself (e.g., to pass to Awaitable.all or check isBound):

def df = new Dataflows()
def v = df.getVariable('x')   // DataflowVariable, does not block
assert !v.isBound()
async { df.x = 42 }
assert await(v) == 42