← Back to Blog
Architecture

Dependency graphs: Nx, Rush, Bazel, docker-compose & CI/CD

Why dependency graphs sit at the heart of modern tooling: monorepo task orchestration, the Affected pattern, docker-compose depends_on, and CI/CD pipelines.

📅 ✍️ Antoine Coulon
graphsdependency-graphmonorepoci-cddocker-compose

Graphs are among the most widely used data structures in computer science, and yet they remain surprisingly underappreciated in practice. Most developers manipulate them daily without realizing it: the moment a tool needs to order tasks, figure out what depends on what, or parallelize work intelligently, odds are a dependency graph is running under the hood.

Before diving into concrete examples, let’s lay the foundations. A graph has its roots in mathematics: it’s an abstract representation of objects together with the relationships they hold between one another. The objects are commonly called nodes (or vertices), and the relationships that connect them are edges (links).

As simple as they may seem, graphs are immensely useful, in large part thanks to the orientation of their edges. A directed edge doesn’t merely express proximity between two nodes: it captures a precise directional relationship: “A depends on B”, “A must run before B”. One thing leading to another, this is exactly what lets you build a dependency graph. And once you hold that graph, you can derive an execution order, detect cycles, or isolate the portions actually impacted by a change.

Here are three families of tools you’re probably already using, all of which rest entirely on this idea.

Nx, Rush, Bazel & co.: monorepo orchestration

Monorepo tools draw one of their main strengths from generalized task orchestration, and that orchestration rests essentially on a dependency graph. The model is straightforward: each project in the monorepo represents a node, and the dependencies between projects are materialized as directed edges between those nodes.

Once this graph is built, two major capabilities become possible.

Determining execution order and parallelizing

Knowing the dependencies between projects lets you compute a valid execution order for tasks (build, test, lint…): a project is only processed once its dependencies have been processed. But more importantly, the graph reveals what is independent. Two projects with no dependency link can be built in parallel without risk, opening the door to significant execution optimizations on large repositories.

The “Affected” pattern: only redo what’s necessary

This is arguably the most spectacular benefit. From the graph, you can determine precisely which projects in the monorepo are affected by a change in a given project, and run tasks (test, build, lint…) only for those projects. No need to rebuild the entire repo when a single leaf library has changed: you walk back up the edges to identify the full set of projects impacted downstream, and ignore the rest.

This is one of the two foundations of the Incremental (or Affected) pattern. The second is managing a cache, local or distributed, that avoids recomputing a task whose result hasn’t changed. The two mechanisms combine to take a monorepo’s CI time from tens of minutes down to a few seconds on targeted changes.

docker-compose: an explicit startup order

The same principle shows up in a tool many people use every day without thinking about it: docker-compose.

In a configuration file, the depends_on option attached to a service explicitly states that service A depends on service B. That relationship is, neither more nor less, a directed edge in a dependency graph.

services:
  web:
    build: .
    depends_on:
      - db
  db:
    image: postgres:16

Concretely, this declaration lets docker-compose honor a sequential startup order rather than the parallel mode that applies by default. The web service won’t be initialized until its dependency, db, has already started.

That’s the crucial point: without this explicit link, docker-compose has no way to know the relationships between services and therefore cannot derive any execution order. The dependency graph isn’t a comfort detail, it’s a prerequisite for any orchestration. As long as the edges aren’t declared, the tool has no choice but to launch everything at once.

CI/CD pipelines: a graph expressed in a DSL

Speaking of task orchestration, what could be more representative than a CI/CD pipeline? Stages and jobs that, depending on the case, must run in parallel or in sequence: it’s a dependency graph in its purest form.

Each platform’s own DSL (GitHub Actions, GitLab CI, etc.) exists precisely to express this graph. In it, you declare that one job waits for another, that one stage follows a previous one, or that a set of tasks can run simultaneously.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: npm run build

  test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: npm test

  deploy:
    needs: [build, test]
    runs-on: ubuntu-latest
    steps:
      - run: npm run deploy

Here, needs plays exactly the role of depends_on: it draws the graph’s edges. From this declaration, the workflow’s orchestrator derives appropriate execution strategies: parallelizing what can be, serializing what must be, and only triggering deploy once build and test have succeeded.

Conclusion

Monorepos, containers, continuous integration: three seemingly unrelated domains, yet all relying on the same fundamental structure. The moment a tool needs to order tasks, parallelize without breaking dependencies, or only reprocess what has actually changed, it’s a dependency graph doing the work.

Understanding this machinery means you stop enduring these tools as black boxes and start reasoning about their behavior: why this job waits, why that project is rebuilt, why this service starts last. Once you’ve seen the underlying graph, you see it everywhere.