Skip to content
Reliable Data Engineering
Practice problem medium graphstopological-sortorchestration
Solve it in the browser (Python editor)

Order Pipeline Tasks in a DAG (Topological Sort)

Difficulty: Medium · Topics: graphs, topological-sort, orchestration · Asked at: Airbnb, Astronomer, Databricks, Uber

Problem

deps maps each task to the list of tasks it depends on. Return a list of stages: each stage is a sorted list of tasks whose dependencies are all in earlier stages (so each stage can run in parallel). If there’s a cycle, raise ValueError("cycle"). Tasks that appear only as dependencies are tasks too.

Examples

deps = {"silver": ["bronze"], "gold": ["silver", "dim"], "dim": ["bronze"], "bronze": []}
→ [["bronze"], ["dim", "silver"], ["gold"]]

Starter code

def stages(deps: dict[str, list[str]]) -> list[list[str]]:
    pass

Hints

Hint 1

Kahn’s algorithm: repeatedly take all nodes with in-degree 0 as one stage.

Hint 2

If nodes remain but none has in-degree 0, there is a cycle.

Solution

from collections import defaultdict

def stages(deps: dict[str, list[str]]) -> list[list[str]]:
    nodes = set(deps) | {d for ds in deps.values() for d in ds}
    indeg = {n: 0 for n in nodes}
    children = defaultdict(list)
    for task, ds in deps.items():
        for d in set(ds):
            indeg[task] += 1
            children[d].append(task)
    ready = sorted(n for n in nodes if indeg[n] == 0)
    result, done = [], 0
    while ready:
        result.append(ready)
        done += len(ready)
        nxt = []
        for n in ready:
            for c in children[n]:
                indeg[c] -= 1
                if indeg[c] == 0:
                    nxt.append(c)
        ready = sorted(nxt)
    if done != len(nodes):
        raise ValueError("cycle")
    return result

Tests

Your solution should pass these:

deps = {"silver": ["bronze"], "gold": ["silver", "dim"], "dim": ["bronze"], "bronze": []}
assert stages(deps) == [["bronze"], ["dim", "silver"], ["gold"]]
assert stages({"b": ["a"]}) == [["a"], ["b"]]
assert stages({}) == []
try:
    stages({"a": ["b"], "b": ["c"], "c": ["a"]})
    assert False
except ValueError as e:
    assert str(e) == "cycle"

Explanation

O(V + E). Level-by-level Kahn’s algorithm gives maximum parallelism stages: exactly how an orchestrator (Airflow scheduler, dbt) decides what can run concurrently. set(ds) guards against duplicate dependency entries inflating in-degrees. The same algorithm orders dbt models and finds downstream tables to backfill (run it on the reversed graph from a node).

Follow-up questions

How would you compute the critical path (minimum total runtime) given task durations?

Process in topological order, finish[t] = duration[t] + max(finish[d] for d in deps[t]); the max finish is the minimum makespan with unlimited parallelism; backtrack the argmax to get the critical path.