skip to content

How do you derive a valid build-and-release order for a set of components from their dependency graph, and why does a single cycle make such an order impossible?

level: middleimportance: should knowfreq 40%

answer

  1. Topological sort = valid build order
  2. Order exists iff graph is a DAG
  3. Kahn: repeatedly take zero-dependency nodes
  4. Cycle = strongly connected component, algorithm stalls
  5. DAG enables parallel builds, caching, impact analysis

basics

~20 s

Topologically sort the dependency graph: build components with no unbuilt dependencies first, then their dependents, and so on. A cycle has no such starting point — each member waits on another — so no valid order exists and the whole loop must be built and released as one unit.

solid answer

~60 s

Model components as nodes and "A depends on B" as an edge A → B. A build order is a **topological sort** of that graph: repeatedly take components whose dependencies are already built (in-degree zero in the reversed graph), build, test and release them, then continue. This also gives the *release* order — you publish versioned artifacts bottom-up, and each level compiles against fixed versions of the level below. A topological order exists **if and only if** the graph is a DAG. Any cycle forms a strongly connected component whose members mutually block: none can be built first, so the algorithm stalls with nodes remaining. Practically, tools react in one of three ways: fail with "circular dependency detected" (Go, many build systems), silently fuse the loop into one compilation unit (slower builds, no isolation), or require a hand-maintained lockstep release. The DAG also tells you *impact*: the transitive dependents of a changed component are exactly what must be rebuilt, retested and re-released — the basis for build caching and affected-target selection in monorepo tooling.

go deeper

for a junior

Say: build things that depend on nothing first, then their dependents — a topological sort — and note a cycle has no starting point.

for a middle

Name the algorithm and the iff condition (topological order exists exactly for DAGs); connect it to bottom-up versioned releases.

for a senior

Extend to impact analysis, affected-target selection, build caching and parallelism; describe the three ways real toolchains react to a cycle.

for a principal

Tie the DAG to deployment and team topology, discuss version-resolution/diamond issues, snapshot-based cycle breaking (bootstrapping), and enforcing acyclicity as a platform-level guarantee.

## The graph Nodes = components (releasable units). Edge `A → B` = "A depends on B" (A's code references B's). To build A you must first have B. ## Topological sort, concretely A **topological order** is a linear ordering of nodes such that every dependency appears before whatever depends on it. Standard algorithm (Kahn's): 1. Compute, for each component, how many of its dependencies are not yet built. 2. Take any component whose count is zero (a leaf — depends on nothing outstanding). Build it, test it, publish a version. 3. Decrement the counts of everything that depended on it. 4. Repeat. 5. If you run out of zero-count components while nodes remain, **the remaining nodes contain a cycle**. That last line is the theorem you should be able to state: **a directed graph has a topological ordering if and only if it is acyclic.** The nodes stuck at the end form one or more *strongly connected components* (sets where every node reaches every other) — precisely the cycles. There is usually more than one valid order; independent branches can be built in any relative order, or **in parallel**. That parallelism is a direct dividend of acyclicity, and the reason build systems represent everything as a DAG. ## Why the order matters beyond compiling - **Release numbering.** Bottom-up ordering means each component is released against *fixed, already-published* versions of its dependencies. Consumers upgrade on their own schedule (release isolation), which is the cure for the morning-after syndrome. - **Impact analysis.** Given a change to component X, the set that must be rebuilt/retested is exactly X plus its transitive **dependents** (follow arrows backwards). Modern build tools (Bazel, Gradle, Nx, Turborepo) use this to build only affected targets and to cache the rest. Cycles collapse those sets and destroy the caching benefit. - **Deployment ordering.** For deployable services the same DAG suggests a safe rollout order and reveals which pairs must be deployed together. ## What a cycle actually does to your build Depending on toolchain, one of: 1. **Hard error.** Go rejects import cycles outright; many build systems, module systems and linkers report "circular dependency detected" and stop. 2. **Silent fusion.** Languages/toolchains that compile whole assemblies (or a monorepo compiled as one unit) simply compile the entire loop together. It works, so nobody notices — but the components are no longer independently buildable, testable, cacheable or releasable. They are one component with several directory names. 3. **Manual lockstep.** Teams start releasing `A 2.1`, `B 2.1`, `C 2.1` simultaneously and forbid mixing versions. That is a coordination tax paid forever. Cycles also break **diamond version resolution** in nasty ways: if A depends on C v1 and B (which A also uses) depends on C v2, resolution is already hard; with a cycle back into A the notion of "the version of A being built" becomes self-referential. ## Edge cases worth knowing - **Bootstrapping/compiler self-hosting** is a real cycle broken by *time*: build version N with the previously released binary of version N-1. That's a snapshot, not a live edge. - **Test-only or dev-only edges** can create cycles that don't exist in production artifacts (A's tests use B, B's main code uses A). Many tools model configurations separately; whether this is acceptable is a team decision — it's harmless for deployment but still slows builds and can confuse tooling. - **Generated code and build scripts** create real edges too; cycles can hide there. - **A cycle in the *runtime* call graph is not a build cycle.** Callbacks, events and DI let calls flow in loops with a perfectly acyclic source graph.

  • Given a change in one component, which components must be rebuilt and retested?
    The changed component plus all of its transitive dependents — follow the dependency arrows backwards. In a DAG this set is usually small and precise; a cycle inflates it to the entire loop and everything above it.
  • A compiler is written in the language it compiles — isn't that a dependency cycle?
    Only across time. Version N is built with the already-released binary of version N-1, so the live graph stays acyclic. The cycle is broken by a snapshot, exactly like depending on a published version rather than a working copy.
  • Does a cycle that exists only between test code and production code count?
    It's a real edge in the build graph and hurts build isolation and tooling, though it doesn't affect deployed artifacts. Many teams model test configurations separately, but the pragmatic advice is still to avoid it.

It's a course prerequisite chart. You can always schedule a degree if prerequisites form a DAG — take the intro courses first, then whatever they unlock. If Calculus II requires Calculus III and vice versa, no student can ever enroll in either; the registrar's only option is to teach both as one giant course.

saying these in an interview costs you the question

  • Claiming a build order can be found for any graph "if you try hard enough" — it exists exactly when the graph is acyclic.
  • Assuming that because the project compiles, there is no cycle — many toolchains simply fuse the loop.
  • Confusing runtime call cycles with build-time dependency cycles.
  • Thinking topological order is unique; independent subgraphs can be ordered arbitrarily or built in parallel.
  • Ignoring build-script, generated-code, or test-configuration edges when reasoning about the graph.

context