Operator & Graph
BLOGE models business logic as a directed acyclic graph of executable nodes. The graph is the unit of orchestration: it defines which operators can run immediately, which ones must wait for upstream outputs, where branch decisions are taken, and how data is assembled between nodes.
Core runtime concepts
| Concept | Role |
|---|---|
Graph | Immutable DAG definition with precomputed source and terminal nodes |
NodeSpec | One executable node, including operator reference, input assembler, metadata, and resilience config |
Operator<I, O> | Business capability contract: one input, one output, one execution boundary |
GraphContext | Request-scoped metadata and caller-provided values for the run |
NodeResults | Thread-safe store of upstream outputs collected during execution |
GraphResult | Final output bundle: node outputs, statuses, errors, elapsed time, and schemas |
A graph in practice
Fluent Java API
Graph graph = Graph.builder("orderProcess")
.node("fetchUser", fetchUserOperator)
.timeout(Duration.ofSeconds(3))
.node("fetchProducts", fetchProductsOperator)
.timeout(Duration.ofSeconds(5))
.node("calcPrice", calcPriceOperator)
.dependsOn("fetchUser", "fetchProducts")
.branch("checkCredit")
.on("approved")
.when(value -> Boolean.TRUE.equals(value), "createOrder")
.otherwise("rejectOrder")
.build();Equivalent DSL
graph orderProcess {
node fetchUser : FetchUserOperator {
input {
userId = ctx.userId
}
timeout = 3s
}
node fetchProducts : FetchProductsOperator {
input {
productIds = ctx.productIds
}
timeout = 5s
}
node calcPrice : CalcPriceOperator {
depends_on = [fetchUser, fetchProducts]
input {
user = fetchUser.output
products = fetchProducts.output
}
}
branch on checkCredit.output.approved {
true -> createOrder
otherwise -> rejectOrder
}
}Both forms describe the same ideas:
- source nodes can start immediately because nothing depends on them
- fan-in nodes wait until all required upstream outputs exist
- branch edges activate only the selected target path
- skipped nodes remain visible in the result model for auditability
What an operator should represent
A BLOGE operator should encapsulate a single business capability with an independent SLA. Good operators are independently testable, observable, and reusable. Examples include:
FetchUserOperatorCreditCheckOperatorNotifyCustomerOperatorRiskAssessmentOperator
BLOGE intentionally avoids turning every field mapping into an operator. Lightweight data shaping belongs in input {} bindings or transform blocks so the graph stays expressive without adding unnecessary execution boundaries.
Data flow: context, input assembly, and results
GraphContext
GraphContext holds request-scoped values such as tenant, trace identifiers, or caller input. It should carry metadata and entry payload, not hidden business state.
GraphContext ctx = new GraphContext(Map.of(
"userId", "u-42",
"productIds", List.of("p-1", "p-2"),
"traceId", "trace-123"
));InputAssembler
Each node assembles its input from two sources:
- upstream node outputs from
NodeResults - request-scoped values from
GraphContext
In Java you provide the assembler directly. In DSL mode the compiler generates the assembler from field bindings such as fetchUser.output.id or ctx.userId.
NodeResults
NodeResults is the execution-time output map. It is thread-safe because multiple ready nodes can finish concurrently on virtual threads.
Branches and transforms
BLOGE separates control flow from data flow:
- use
branch on ...to decide which downstream nodes should execute - use
transformto compute reusable pure data projections without inventing a new business operator
transform orderSummary {
customerName = fetchUser.output.name
total = calcPrice.output.total
premiumTier = when {
fetchUser.output.vipLevel > 3 -> "premium"
otherwise -> "standard"
}
}transform blocks behave like lightweight virtual nodes: they are observable in the graph model, but they do not represent an external business capability.
Extension points
BLOGE keeps the core small and exposes extension points through SPI interfaces:
| SPI | Use case |
|---|---|
OperatorInterceptor | Around-invocation hooks for tracing, authorization, or custom policies |
ExecutionListener | Lifecycle callbacks for graph and node events |
OperatorRegistry | Custom operator lookup or discovery |
ExpressionFunction | Extend the DSL function library |
These APIs let you add cross-cutting behavior without changing graph definitions.
Design guidance
Keep graphs clear by following a few simple rules:
- Make capability boundaries visible. If a step deserves its own timeout or retry policy, it probably deserves its own operator.
- Prefer transforms for data shaping. Do not create operators for field renaming, basic formatting, or small projections.
- Keep the graph acyclic. Feedback loops should use later stages, waits, or explicit loop constructs instead of circular dependencies.
- Model the happy path and the degraded path. Fallbacks, otherwise branches, and skipped nodes should be deliberate and observable.
Next steps
- Learn how the scheduler runs these graphs in Execution Model
- Configure retries, timeouts, and fallbacks in Resilience Policies
- Dive into the authoring surface in DSL Overview