Online edition for BLOGE
0.9.8-RC1· facts verified 2026-09-15 · 中文
Chapter 20 — Spring and Production Wiring
Promise: By the end of this chapter you will be able to start one BLOGE graph through Spring Boot with no manual engine bean, read the health result, and apply one deliberate
GraphEngineCustomizeroverride without taking ownership of the whole auto-configuration graph.
Learning Goals
- Add the
bloge-springdependency and letBlogeAutoConfigurationwire aGraphEngine,OperatorRegistry,GraphLoader, and compiledList<Graph>with zero manual bean definitions. - Annotate runtime
Operator,SuspendableOperator, orStreamingOperatorimplementations with@BlogeOperatorso Spring discovers and registers them. - Inject
GraphEngineand the compiled graph list into a service class, execute a graph, and return results from an HTTP endpoint. - Read
UP + graphCount=1as a wiring observation, not as business proof. - Apply one lightweight-engine override with
GraphEngineCustomizer, and know when replacing a bean would create unnecessary ownership.
Prerequisites
- Chapter 7 — Designing Good Operators — you need the operator design model before wiring operators into Spring.
- Chapter 13 — Durable Execution — runtime stores, checkpoints, and suspend/resume, since the starter conditionally wires all of them.
- Familiarity with Spring Boot auto-configuration,
@ConfigurationProperties, and Spring Boot Actuator basics.
Source Examples
| File | What it shows |
|---|---|
BlogeAutoConfiguration.java | Entry auto-configuration that imports the core slice |
BlogeProperties.java | All spring.bloge.* configuration properties and nested groups |
BlogeOperator.java | The @Component-composed annotation for operator scanning |
BlogeObservabilityAutoConfiguration.java | Metrics, tracing, MDC, health indicator wiring |
BlogeEndpointAutoConfiguration.java | /actuator/bloge and /actuator/blogeStats endpoints |
BlogeAuditAutoConfiguration.java | Audit journal listener activation |
SessionExecutorAutoConfiguration.java | SessionExecutor wiring with access guards and listeners |
spring-ticket-triage.bloge | Minimal DSL loaded by the starter example |
ch19/loan-approval.bloge | Loan approval graph wired with Spring operators |
SpringBootTicketTriageApplication.java | End-to-end Spring Boot application using the starter |
SpringTicketTriageService.java | Service layer injecting GraphEngine and List<Graph> |
Why This Matters
In earlier chapters you built engines by hand:
var engine = GraphEngine.builder()
.registry(registry)
.executionCheckpointStore(checkpointStore)
.listeners(listeners)
.build();
That's fine for tests and learning. In production, the wiring list grows: interceptors, listeners, context carriers, durable stores, checkpoint codecs, recovery config, shard resolvers, timer services, event deduplication — and every bean has conditions and fallbacks.
The bloge-spring module addresses this assembly work. It is a Spring Boot
Starter that auto-configures components whose classpath and property conditions
are satisfied. Declaring operators as Spring beans and placing .bloge files
on the classpath lets the starter assemble a runnable engine for that
application slice. Adding bloge-durable or bloge-metrics-otel lets the
starter detect and wire their conditional beans without repeated @Bean
declarations. This establishes application wiring; it does not prove that
durable stores, external effects, or production capacity are ready.
Key insight: The starter doesn't hide the engine — it assembles it. Most replaceable singleton defaults use
@ConditionalOnMissingBean; extension collections and endpoint slices follow their own conditions, so confirm the target bean's contract before overriding it.
The Shortest Wiring Path
Do not begin with every optional production feature. Begin with one vertical slice: one dependency, one annotated operator, one classpath DSL, one injected executor, and one health observation.
| You own | The starter owns | Observable check |
|---|---|---|
@BlogeOperator bean implementing a runtime operator contract | OperatorRegistry | operator name resolves during DSL load |
classpath:/bloge/*.bloge | GraphLoader and compiled List<Graph> | expected graph is present |
| service input and output | lightweight GraphEngine / GraphExecutor | one execution returns the expected node facts |
| deployment dependencies | BlogeHealthIndicator when Actuator is present | UP, graphCount=1 |
UP here means the application loaded at least one graph. It does not mean an
order Scenario passed, an external payment system is healthy, or a release is
qualified.
Mental Model
Think of bloge-spring as a layered assembly line for beans. Each layer
activates only when its classpath and property prerequisites are met:
Layers 2–6 are additive and independent. You never need all of them at once, and removing a classpath dependency removes the layer cleanly.
First Working Example
This is the ticket-triage example from
bloge-examples/src/main/java/com/leanowtech/bloge/examples/integration/spring/.
Step 1 — Add the dependency
<dependency>
<groupId>com.leanowtech.bloge</groupId>
<artifactId>bloge-spring</artifactId>
<version>${bloge.version}</version>
</dependency>
Step 2 — Write an operator as a Spring bean
Simplified from SpringTicketClassifierOperator.java:
@BlogeOperator(
value = "SpringTicketClassifierOperator",
description = "Classifies a support ticket",
owner = "examples",
tags = {"spring", "starter", "triage"}
)
public class SpringTicketClassifierOperator implements
Operator<Map<String, Object>, ClassifiedTicket> {
public record ClassifiedTicket(String queue, int priorityScore, boolean vip) {}
@Override
public ClassifiedTicket execute(Map<String, Object> input, OperatorContext ctx) {
boolean vip = "vip".equalsIgnoreCase(String.valueOf(input.get("customerTier")));
String queue = vip ? "vip-escalation" : "general-support";
int priorityScore = vip ? 70 : 30;
return new ClassifiedTicket(queue, priorityScore, vip);
}
}
@BlogeOperator is composed from @Component, so Spring discovers the bean.
The registry then accepts it only when the bean implements Operator,
SuspendableOperator, or StreamingOperator. The value attribute sets the
DSL operator name. The repository example deliberately keeps its domain beans
plain and supplies adapters through an explicit OperatorRegistry; the compact
version above chooses the zero-manual-registry path instead.
Step 3 — Place the DSL on the classpath
From spring-ticket-triage.bloge:
graph springTicketTriage {
node classifyTicket : SpringTicketClassifierOperator {
input {
ticketId = ctx.ticketId
message = ctx.message
customerTier = ctx.customerTier
}
timeout = 2s
}
node draftReply : SpringReplyDraftOperator {
depends_on = [classifyTicket]
input {
ticketId = ctx.ticketId
queue = classifyTicket.output.queue
priorityScore = classifyTicket.output.priorityScore
vip = classifyTicket.output.vip
}
timeout = 2s
}
}
The starter's default DSL location is classpath:bloge/. Place this file at
src/main/resources/bloge/spring-ticket-triage.bloge and it will be loaded
automatically.
Step 4 — Inject and execute
From SpringTicketTriageService.java:
@Service
public class SpringTicketTriageService {
private final GraphEngine engine;
private final Map<String, Graph> graphsByName;
public SpringTicketTriageService(GraphEngine engine, List<Graph> graphs) {
this.engine = engine;
this.graphsByName = graphs.stream()
.collect(Collectors.toUnmodifiableMap(Graph::name, Function.identity()));
}
public TicketTriageResponse triage(String ticketId, String message, String customerTier) {
Graph graph = graphsByName.get("springTicketTriage");
var result = engine.execute(graph, new GraphContext(Map.of(
"ticketId", ticketId,
"message", message,
"customerTier", customerTier
)));
return /* map result to response */;
}
}
No GraphEngine.builder(). No DefaultOperatorRegistry. No GraphLoader
calls. The starter assembled all of it.
Step 5 — Expose via HTTP
From SpringTicketTriageController.java:
@RestController
@RequestMapping("/api/bloge/tickets")
public class SpringTicketTriageController {
private final SpringTicketTriageService triageService;
public SpringTicketTriageController(SpringTicketTriageService triageService) {
this.triageService = triageService;
}
@GetMapping("/triage")
public TicketTriageResponse triage(
@RequestParam("ticketId") String ticketId,
@RequestParam("message") String message,
@RequestParam(name = "customerTier", defaultValue = "standard") String customerTier) {
return triageService.triage(ticketId, message, customerTier);
}
}
One Override, One Explicit Owner
Suppose the default lightweight engine is correct except for one scheduler integration. Keep every starter-owned bean and customize only the builder seam:
@Bean
GraphEngineCustomizer schedulerOverride(MyTimerSupport timerSupport) {
return builder -> builder.schedulerTimerSupport(timerSupport);
}
The customizer runs after the lightweight engine-mode preset and before the
core GraphEngine is built. It does not configure the durable runtime path.
If you replace the whole GraphEngine bean, you also accept responsibility for
its listeners, interceptors, context carriers, and future starter defaults.
The remainder of this chapter is a production reference. The first-read path is complete once the minimal slice, health boundary, and single override are clear.
Production Reference
The shortest path above is enough to understand ownership. When you need the complete bean inventory, property groups, durable-store wiring, health checks, or HTTP error contract, continue with Appendix I — Spring Production Reference.
Common Trap
❌ Registering operators manually when @BlogeOperator already exists
// DON'T do this if you also annotate the class with @BlogeOperator:
@Bean
public OperatorRegistry operatorRegistry() {
var registry = new DefaultOperatorRegistry();
registry.register("FetchUserOperator", new FetchUserOperator(userService));
return registry;
}
Because BlogeAutoConfiguration provides an OperatorRegistry bean that scans
for @BlogeOperator-annotated beans, and its bean is
@ConditionalOnMissingBean, declaring your own OperatorRegistry bean replaces
the auto-configured one entirely. Your custom registry won't scan for
@BlogeOperator beans — so all annotated operators silently disappear.
Fix: Either rely entirely on @BlogeOperator scanning (the normal path), or
provide a complete OperatorRegistry bean that includes everything you need.
Don't mix both approaches.
What Goes Wrong
Silent operator disappearance
You annotate a class with @BlogeOperator("MyOperator") but also define a
custom OperatorRegistry bean:
@Bean
public OperatorRegistry operatorRegistry() {
return new DefaultOperatorRegistry(); // empty — no scanning
}
The auto-configured scanning registry is skipped because your bean exists. At runtime:
GraphDefinitionException: Node 'doWork' references unregistered operator 'MyOperator'
Fix: Either rely on auto-scanning (remove the custom @Bean) or populate
your custom registry with all required operators.
One Production Extension at a Time
Add observability, audit, recovery, or an interceptor only when a stated operational need requires it. Appendix I provides the configuration map and a deployment checklist; keep this chapter's design rule: each override must have one reason and one owner.
Brain Check
-
What is required for automatic operator registration? (Answer: the bean is marked with
@BlogeOperatorand implementsOperator,SuspendableOperator, orStreamingOperator; the annotation alone only guarantees Spring component discovery.) -
Where does the starter look for
.blogefiles by default, and which property overrides this? (Answer:classpath:bloge/, configurable viaspring.bloge.dsl-locations.) -
If you declare your own
OperatorRegistrybean, what happens to the auto-configured one? (Answer: it is skipped —@ConditionalOnMissingBeanmeans your bean wins, and automatic@BlogeOperatorscanning no longer occurs.) -
Name two production features that activate purely by adding a classpath dependency, with no property changes. (Answer: Actuator endpoints activate when
spring-boot-actuatoris present; observability metrics and tracing activate whenbloge-metrics-otelis present.) -
What does
BlogeHealthIndicatorreport when no.blogefiles are found? (Answer:DOWNwith detail"reason": "No graphs loaded".) -
Design question: Your team uses three separate Spring Boot microservices, each with its own
bloge-springdependency and different graphs. Should each service have its ownGraphEngineinstance, or should they share one central engine? (Each service should have its ownGraphEngine. The engine is stateless and lightweight — there is no benefit to centralising it. Sharing a database for durable stores is fine if the services need cross-service visibility, but the engines themselves are independent.)
Lab
Starting from a fresh Spring Boot application:
-
Add
bloge-springto your POM and create oneOperatorimplementation annotated with@BlogeOperatorthat returns a greeting string. -
Write a one-node
.blogegraph insrc/main/resources/bloge/that references your operator. -
Create a
@Servicethat injectsGraphEngineandList<Graph>, looks up your graph by name, and executes it. -
Verify the health indicator: start the application and hit
/actuator/health. Confirm theblogecomponent showsUPwith your graph name. -
Break it intentionally: rename the
.blogefile so no graphs load. Restart and verify the health indicator reportsDOWN. -
Add a custom
ExecutionListenerbean that logs graph-level completion times. Execute your graph again and confirm the listener fires without any changes to the engine bean.
Experiment acceptance card
- Expected and observed: The starter wires engine, loader, and health with one owner per customizer.
- Failure and recovery: Remove a bean or duplicate an override; recover minimal auto-configuration.
- Proof boundary: Proves Spring wiring and health, not production capacity or business correctness.
- Exercise contract: Minimal app; change one bean/customizer; deliver context and health; stop with successful startup and one owner.
Recap
bloge-springis a Spring Boot Starter that can auto-configureGraphEngine,OperatorRegistry,GraphLoader, and compiled graphs when their classpath, property, and bean conditions are satisfied.@BlogeOperatoris a@Component-composed annotation. Annotated beans are registered automatically only when they implement one of BLOGE's runtime operator contracts; plain domain beans need an adapter/customizer path. ItspromptHint,usageExample, andconstraintsDescriptionattributes support agent/LLM integration.- The starter uses layered, conditional auto-configuration: core beans are
present inside an active starter context; durable stores activate when
bloge-durable+DataSourceexist; observability activates whenbloge-metrics-otelexists; audit and recovery activate when their respective properties are set totrue. - The auto-configuration is split into focused slices
(
BlogeCoreAutoConfiguration,BlogeGraphEngineAutoConfiguration,BlogeDurableAutoConfiguration, etc.) for cleaner separation. - New
spring.bloge.engine-modeproperty selects engine presets (AUTOorREQUEST_RESPONSE). - New
spring.bloge.tenant.*properties enable auto-configured multi-tenant support — tenant resolution, namespace resolution, concurrency caps, and rate limits — without writing a customTenantContextResolverbean. - New
spring.bloge.version-routing.*properties supportlatest,pinned, andcanarygraph version routing at the starter level. GraphEngineCustomizerlets you fine-tune the engine builder without replacing the entire bean.- Most replaceable singleton defaults are guarded by
@ConditionalOnMissingBean; inspect the specific slice before overriding it. - Custom
OperatorInterceptor,ExecutionListener, andCallerContextCarrierbeans are collected automatically viaObjectProviderand injected into the engine. - Actuator endpoints (
/actuator/bloge,/actuator/blogeStats) give runtime visibility into loaded graphs and execution metrics. BlogeHealthIndicatorprovides/actuator/healthintegration out of the box.
Next Step
In Chapter 21 — Scheduling and Complexity in One JVM, you will isolate the local scheduling mechanism: ready sets, completion events, and graph-complexity guards. Distributed ownership and capacity remain in Chapter 22.
Reference Links
bloge-springmodule README — full property tables and quick-start guideBlogeAutoConfiguration.java— core auto-configuration sourceBlogeProperties.java— allspring.bloge.*propertiesBlogeObservabilityAutoConfiguration.java— metrics, tracing, health indicatorBlogeEndpointAutoConfiguration.java— Actuator endpointsBlogeAuditAutoConfiguration.java— audit journalingSpringBootTicketTriageApplication.java— complete Spring Boot exampleAutoConfiguration.imports— starter registration file- Getting Started — setup guide
- Core Architecture — engine internals reference
Coding Agent: Open the versioned task guide.