Skip to content

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

ConceptRole
GraphImmutable DAG definition with precomputed source and terminal nodes
NodeSpecOne executable node, including operator reference, input assembler, metadata, and resilience config
Operator<I, O>Business capability contract: one input, one output, one execution boundary
GraphContextRequest-scoped metadata and caller-provided values for the run
NodeResultsThread-safe store of upstream outputs collected during execution
GraphResultFinal output bundle: node outputs, statuses, errors, elapsed time, and schemas

A graph in practice

Fluent Java API

java
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

bloge
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:

  • FetchUserOperator
  • CreditCheckOperator
  • NotifyCustomerOperator
  • RiskAssessmentOperator

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.

java
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 transform to compute reusable pure data projections without inventing a new business operator
bloge
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:

SPIUse case
OperatorInterceptorAround-invocation hooks for tracing, authorization, or custom policies
ExecutionListenerLifecycle callbacks for graph and node events
OperatorRegistryCustom operator lookup or discovery
ExpressionFunctionExtend 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:

  1. Make capability boundaries visible. If a step deserves its own timeout or retry policy, it probably deserves its own operator.
  2. Prefer transforms for data shaping. Do not create operators for field renaming, basic formatting, or small projections.
  3. Keep the graph acyclic. Feedback loops should use later stages, waits, or explicit loop constructs instead of circular dependencies.
  4. Model the happy path and the degraded path. Fallbacks, otherwise branches, and skipped nodes should be deliberate and observable.

Next steps