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
- Distinguish implicit dependencies (inferred from
.outputreferences) from explicit dependencies (depends_on). - Predict which nodes run in parallel and which wait, given a graph definition.
- Understand how the engine's completion-driven scheduler turns a DAG into an execution plan.
- Recognise when an unnecessary dependency kills parallelism.
Prerequisites
Source Examples
| File | What it shows |
|---|---|
ch03/order-process.bloge | Fan-out, fan-in, and branching in a realistic workflow |
ch03/bff-dashboard.bloge | Five-way parallel fan-out with mixed resilience |
depends-timeout.bloge | Explicit 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.)
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:
| Style | Syntax | When to use |
|---|---|---|
| Implicit | Reference someNode.output in an input binding | Most of the time — the compiler infers the edge automatically |
| Explicit | depends_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:
| Node | Depends on | Why |
|---|---|---|
fetchUser | (nothing) | Only reads ctx — a source node |
fetchProducts | (nothing) | Only reads ctx — a source node |
calcPrice | fetchUser, fetchProducts | Explicit depends_on plus .output references |
Execution timeline:
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.
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:
| Observation | Required inequality |
|---|---|
| Both sources are eligible together | start(fetchUser) and start(fetchProducts) occur before either is released |
| One completed source is insufficient | start(calcPrice) remains unset while the other source is blocked |
| Fan-in starts after both | start(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:
Questions:
- Can
checkInventoryrun in parallel withcheckCredit? (Yes, ifcheckInventorydepends oncalcPriceor earlier, but not oncheckCredit.) - What should the branch fan-in from? (Both
checkCreditandcheckInventory, so you'd reference both outputs.) - 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
- What is a source node? (A node with no incoming dependencies — it reads
only from
ctx.) - If node C references
A.outputandB.outputin its input block, what is the minimum set of dependencies the engine infers? (C depends on A and C depends on B.) - 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.)
- If you add
depends_on = [fetchUser]to a node that only readsctx, what happens? (The node is forced to wait forfetchUsereven though it doesn't need the data — accidental serialisation.) - 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 adddepends_on = [A, B]explicitly, or is the implicit dependency fromA.outputandB.outputenough? (The implicit dependency from.outputreferences is sufficient — adding explicitdepends_onwould be redundant. Only use explicit dependencies when you need ordering without consuming the output.)
Lab
- Open
ch03/bff-dashboard.bloge. - Draw the dependency graph on paper (or in ASCII). Mark source nodes with
[S]and the terminal node with[T]. - 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.)
- Now add a
fetchWishlistnode that depends onfetchProfile.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
.outputreferences) 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.
Reference Links
- Core Architecture — engine scheduling design
- DSL Specification —
depends_onand input binding grammar ch03/order-process.bloge— sourcech03/bff-dashboard.bloge— sourcedepends-timeout.bloge— conformance fixture
Coding Agent: Open the versioned task guide.