DAGs: workflows and dependencies

Topics in this section
2 cards

Directed acyclic graphs

A DAG is a directed graph with no cycles: following the arrows, you can never return to where you started. Unlike a tree, a node can have several parents. That's exactly the shape of anything with dependencies: a step that can start only after two others have finished.

Because there are no cycles, the nodes can always be lined up in a topological order: an order where every node comes after everything it depends on. That order is the plan of execution. Steps with no path between them, like 'Manager OK' and 'Budget check' in the figure, can run in parallel.

An invoice approval DAG where Pay waits for both Manager OK and Budget check, beside a cycle that deadlocks
In real life

An invoice can be paid only after the manager approves it and the budget check passes, and both of those need the invoice to be submitted first. Build systems (compile before link), spreadsheet formulas, and data pipelines in tools like Airflow are all DAGs.

Check yourself

In a DAG, the step 'Pay' has two incoming edges: from 'Manager OK' and from 'Budget check'. What does that mean?

Topological order and cycle detection

To compute a topological order, count each node's incoming edges (its in-degree). Start with the nodes at zero: they depend on nothing. Take one, add it to the order, and remove its outgoing edges; any node whose count drops to zero is now ready. Repeat. This is Kahn's algorithm, O(nodes + edges).

It also detects cycles for free. If the queue empties before every node is placed, the remaining nodes are in a loop or wait on one, and none can ever start. In a workflow that's a deadlock; in a build it's a circular dependency; in a spreadsheet it's a circular reference error.

In real life

Build tools order their steps this way; cargo, for example, refuses to build when two crates depend on each other. Excel shows 'circular reference' for the same reason.

In code: Python · Rust
from graphlib import TopologicalSorter, CycleError   # standard library, 3.9+
steps = {
    "Manager OK": {"Submit"},          # step: the steps it waits for
    "Budget check": {"Submit"},
    "Pay": {"Manager OK", "Budget check"},
}
print(list(TopologicalSorter(steps).static_order()))
# ['Submit', 'Manager OK', 'Budget check', 'Pay'] (the middle two in either order)

try:
    list(TopologicalSorter({"A": {"B"}, "B": {"A"}}).static_order())
except CycleError:
    print("deadlock: A and B wait for each other")
  • Python ships graphlib.TopologicalSorter; it raises CycleError on a loop.
  • The Rust version scans all edges per node to stay short; store an adjacency list to make it O(nodes + edges).
Check yourself

Kahn's algorithm stops with three steps never placed in the order. What does that tell you?