Skip to main content

Online edition for BLOGE 0.9.8-RC1 · facts verified 2026-09-15 · 中文

Chapter 3 — Thinking in Dependencies

Promise: By the end of this chapter you will know how to read a graph as a dependency structure, predict its execution order, and stop thinking in sequential steps.


Learning Goals

  1. Distinguish implicit dependencies (inferred from .output references) from explicit dependencies (depends_on).
  2. Predict which nodes run in parallel and which wait, given a graph definition.
  3. Understand how the engine's completion-driven scheduler turns a DAG into an execution plan.
  4. Recognise when an unnecessary dependency kills parallelism.

Prerequisites

Source Examples

FileWhat it shows
ch03/order-process.blogeFan-out, fan-in, and branching in a realistic workflow
ch03/bff-dashboard.blogeFive-way parallel fan-out with mixed resilience
depends-timeout.blogeExplicit dependency + timeout

Why This Matters

In Chapter 2 you ran a graph with one or two nodes. That was enough to learn the mechanics. But real workflows have many nodes with complex dependency relationships — and the execution order is no longer obvious from reading top to bottom.

If you don't understand how the engine resolves dependencies, you will:

  • Accidentally serialise nodes that could run in parallel.
  • Introduce circular dependencies that the compiler rejects.
  • Struggle to debug timing because you expect a sequence that the engine never promised.

The shift in thinking is: you declare what depends on what; the engine decides when to run what.


Mental Model

The DAG as a promise

A BLOGE graph is a directed acyclic graph (DAG). Each node is a vertex; each dependency is a directed edge.

(The diagram below shows the same structure as a rendered graph.)

Diagram: 03-thinking-in-dependencies figure 1

Diagram: 03-thinking-in-dependencies figure 2

Reading this DAG:

  • A and B have no incoming edges → they are source nodes → the engine starts them immediately, in parallel.
  • C depends on both A and B → it waits for both to complete.
  • D depends on B only → it starts as soon as B finishes (even if A is still running).
  • E depends on C and D → it waits for both.

This is the completion-driven scheduling model. When a node completes, the engine checks whether any downstream node now has all its dependencies satisfied. If yes, that node is dispatched.

Implicit vs. explicit dependencies

There are two ways to create an edge:

StyleSyntaxWhen to use
ImplicitReference someNode.output in an input bindingMost of the time — the compiler infers the edge automatically
Explicitdepends_on = [someNode]When you need ordering but don't consume the output

Both produce the same runtime edge. Implicit is more common because you usually do need the upstream data.


First Working Example

Look at the dependency structure of ch03/order-process.bloge:

graph orderProcess {

node fetchUser : FetchUserOperator {
input { userId = ctx.userId }
timeout = 3s
retry = { attempts: 2, backoff: 200ms, strategy: exponential }
}

node fetchProducts : FetchProductsOperator {
input { productIds = ctx.productIds }
timeout = 5s
}

node calcPrice : CalcPriceOperator {
depends_on = [fetchUser, fetchProducts]
input {
user = fetchUser.output
products = fetchProducts.output
}
}

// ...
}

Dependency analysis:

NodeDepends onWhy
fetchUser(nothing)Only reads ctx — a source node
fetchProducts(nothing)Only reads ctx — a source node
calcPricefetchUser, fetchProductsExplicit depends_on plus .output references

Execution timeline:

Diagram: 03-thinking-in-dependencies figure 3

fetchUser and fetchProducts start at the same moment. calcPrice starts only after both finish. The graph declaration expresses this dependency shape, so business code does not need to assemble a thread pool or CompletableFuture.allOf(). Runtime executor capacity remains an application configuration concern.

Java API Equivalent

The same dependency shape built with the fluent Java API. This excerpt mirrors the real builder style used in bloge-examples/.../OrderProcessingExample.java:

var builder = Graph.builder("orderProcess")
.node("fetchUser", FETCH_USER)
.input((results, ctx) -> new UserQuery(ctx.get("userId", String.class)))
.timeout(Duration.ofSeconds(3))
.retry(2, Duration.ofMillis(200), BackoffStrategy.EXPONENTIAL)
.node("fetchProducts", FETCH_PRODUCTS)
.input((results, ctx) -> new ProductQuery(ctx.get("productIds", List.class)))
.timeout(Duration.ofSeconds(5))
.node("calcPrice", CALC_PRICE)
.dependsOn("fetchUser", "fetchProducts")
.input((results, ctx) -> new PriceInput(
results.get("fetchUser", User.class),
results.get("fetchProducts", ProductList.class)));

Graph graph = builder.build();

The DSL version and the Java version produce the same runtime graph. The DSL is typically preferred for authoring workflows; the Java API is used for programmatic graph construction and testing.


Watch the Ready Set Move in Four Frames

"A and B run in parallel" is easy to repeat and surprisingly easy to misunderstand. The scheduler does not move a cursor from the first line to the last. It maintains a set of nodes whose dependencies are satisfied, then reacts to completion events.

Diagram: four-frame ready-set trace

What the timestamps prove

The times in the diagram are a rounded, normalised trace; their exact values are not a performance promise. The RC1 probe asserts the ordering relationships:

ObservationRequired inequality
Both sources are eligible togetherstart(fetchUser) and start(fetchProducts) occur before either is released
One completed source is insufficientstart(calcPrice) remains unset while the other source is blocked
Fan-in starts after bothstart(calcPrice) > start(fetchUser) and start(calcPrice) > start(fetchProducts)

At 42 ms, fetchUser is finished but calcPrice still has one unresolved dependency. At 67 ms, the second completion decrements that count to zero and adds calcPrice to the ready set. This is the technical mechanism behind the visual fan-in.

Change one edge, predict one consequence

Remove the dependency on fetchProducts and calcPrice may start at 42 ms, before product data exists. Add an unnecessary edge from fetchUser to fetchProducts and the two source calls become serial. A dependency is therefore not a drawing preference: it is an executable claim about data and ordering.


Break It Apart

Fan-out

When multiple source nodes read only from ctx, the engine starts them all at once. This is fan-out:

// Five independent fetches — all run in parallel
node fetchProfile : FetchProfileOperator { input { userId = ctx.userId } }
node fetchOrders : FetchOrdersOperator { input { userId = ctx.userId } }
node fetchRecommendations : FetchRecommendationsOperator { input { userId = ctx.userId } }
node fetchNotifications : FetchNotificationsOperator { input { userId = ctx.userId } }
node fetchLoyalty : FetchLoyaltyOperator { input { userId = ctx.userId } }

(From ch03/bff-dashboard.bloge.)

Fan-in

When a node references multiple upstream outputs, it fans in — it waits for all of them:

node aggregate : AggregateOperator {
depends_on = [fetchProfile, fetchOrders, fetchRecommendations, fetchNotifications, fetchLoyalty]
input {
profile = fetchProfile.output
orders = fetchOrders.output
recommendations = fetchRecommendations.output
notifications = fetchNotifications.output
loyalty = fetchLoyalty.output
}
}

Explicit dependency without data

Sometimes you need ordering without consuming the output. For example, you want node B to run after node A completes, even though B doesn't use A's data:

node a : OpA {}
node b : OpB {
depends_on = [a]
}

(From depends-timeout.bloge.)

The explicit depends_on creates the same kind of edge as an implicit .output reference.


Common Trap

❌ Accidental serialisation

node fetchUser : FetchUserOperator {
input { userId = ctx.userId }
}

node fetchProducts : FetchProductsOperator {
// WRONG — this creates an unnecessary dependency on fetchUser
depends_on = [fetchUser]
input { productIds = ctx.productIds }
}

Because fetchProducts only needs ctx.productIds, the depends_on is unnecessary. It forces fetchProducts to wait for fetchUser, killing parallelism.

Rule of thumb: Only declare a dependency when you genuinely need the upstream node to finish first — either because you need its data or because of a side-effect ordering requirement.


What Goes Wrong

Circular dependency — the compiler rejection

If you accidentally create a cycle:

node a : OpA {
input { x = b.output.value }
}
node b : OpB {
input { y = a.output.value }
}

the compiler rejects the graph with a GraphDefinitionException. The exact message includes cycle, because the dependency graph can no longer be topologically ordered:

GraphDefinitionException: ... cycle ...

Fix: Redesign so data flows in one direction. If two nodes genuinely need each other's output, introduce an intermediate node or rethink the decomposition.


Guided Rewrite

Take the order-process graph and imagine a new requirement: before creating the order, you must check inventory and credit, independently.

Sketch the dependencies:

Diagram: 03-thinking-in-dependencies figure 4

Questions:

  1. Can checkInventory run in parallel with checkCredit? (Yes, if checkInventory depends on calcPrice or earlier, but not on checkCredit.)
  2. What should the branch fan-in from? (Both checkCredit and checkInventory, so you'd reference both outputs.)
  3. What is the minimum wall-clock time if all nodes take 1 second? (3 seconds: one for the fetches in parallel, one for calcPrice, one for the checks in parallel, then the branch is essentially instantaneous.)

Brain Check

  1. What is a source node? (A node with no incoming dependencies — it reads only from ctx.)
  2. If node C references A.output and B.output in its input block, what is the minimum set of dependencies the engine infers? (C depends on A and C depends on B.)
  3. Why does the engine use a DAG and not allow cycles? (A cycle would mean a node waits for itself — the graph could never complete. The compiler detects cycles using Kahn's algorithm and rejects them.)
  4. If you add depends_on = [fetchUser] to a node that only reads ctx, what happens? (The node is forced to wait for fetchUser even though it doesn't need the data — accidental serialisation.)
  5. Design question: You have a graph where nodes A and B are independent source nodes, and node C depends on both. A new requirement says C should also use data from the ctx. Should you add depends_on = [A, B] explicitly, or is the implicit dependency from A.output and B.output enough? (The implicit dependency from .output references is sufficient — adding explicit depends_on would be redundant. Only use explicit dependencies when you need ordering without consuming the output.)

Lab

  1. Open ch03/bff-dashboard.bloge.
  2. Draw the dependency graph on paper (or in ASCII). Mark source nodes with [S] and the terminal node with [T].
  3. Predict the execution timeline. If each fetch takes 200 ms and the aggregate takes 50 ms, what is the total wall-clock time? (≈ 250 ms — all fetches run in parallel, then aggregate runs.)
  4. Now add a fetchWishlist node that depends on fetchProfile.output.userId. Re-draw the graph. What is the new wall-clock time? (≈ 450 ms — fetchWishlist must wait for fetchProfile, then aggregate waits for all six.)

Experiment acceptance card

  • Expected and observed: Two sources enter the ready set together; fan-in starts after both complete.
  • Failure and recovery: Add a dependency between sources; remove it to restore parallelism.
  • Proof boundary: Proves dependency-driven scheduling, not external latency or thread safety.
  • Exercise contract: Pricing graph; change one depends_on edge; deliver before/after timelines; stop when starts match the prediction.

Recap

  • A BLOGE graph is a DAG. Nodes are vertices; dependencies are directed edges.
  • Dependencies can be implicit (from .output references) or explicit (depends_on).
  • The engine uses a completion-driven scheduler: when a node finishes, it checks which downstream nodes are now ready.
  • Fan-out happens when multiple source nodes have no dependencies on each other.
  • Fan-in happens when a node waits for multiple upstream nodes.
  • Unnecessary dependencies kill parallelism — only declare them when you genuinely need ordering.

Next Step

In Chapter 4 — Data That Flows you will learn how data moves through the graph — input bindings, output references, path expressions, transforms, and when to use an operator vs. a projection without Operator scheduling projection.


Coding Agent: Open the versioned task guide.