Online edition for BLOGE
0.9.8-RC1· facts verified 2026-09-15 · 中文
Appendix I — Spring Production Reference
Use this appendix after Chapter 20 works. It is a lookup surface for production assembly, not a second tutorial path.
Return to Chapter 20 when the ownership model is unclear.
Break It Apart
What the starter auto-configures
The auto-configuration is registered via the Spring Boot 3.x
AutoConfiguration.imports
file, which lists ten top-level auto-configuration entries. The table also shows
BlogeCoreAutoConfiguration, which BlogeAutoConfiguration imports internally:
| Configuration class | What it provides | Gate condition |
|---|---|---|
BlogeAutoConfiguration | Entry point — imports BlogeCoreAutoConfiguration | Always active |
BlogeCoreAutoConfiguration | OperatorRegistry, GraphLoader, List<Graph>, tenant resolver, tenant filter | Always active |
BlogeDurableMigrationConfiguration | Explicit encrypted-checkpoint migration tooling | bloge.decrypt-checkpoints=true + migration classes on classpath |
BlogeGraphEngineAutoConfiguration | GraphEngine via builder with all ObjectProvider collaborators | Active unless checkpoint decryption migration mode is enabled |
BlogeDurableAutoConfiguration | All durable stores, RecoveryConfig, RoutingStore, Flyway migrations | bloge-durable on classpath + DataSource bean |
BlogeAuditAutoConfiguration | AuditJournalListener | spring.bloge.audit.enabled=true + AuditJournalStore bean present |
SessionExecutorAutoConfiguration | Pure SessionExecutor; optional durable session adapter and manager | bloge-session-ext on classpath; durable beans require durable classes plus stores and definition lookup |
BlogeStateMachineAutoConfiguration | Durable state-machine manager and recovery wiring | durable state-machine class on classpath + spring.bloge.state-machine.enabled=true |
BlogeEndpointAutoConfiguration | /actuator/bloge, /actuator/blogeStats | spring-boot-actuator on classpath |
BlogeObservabilityAutoConfiguration | Metrics, tracing, MDC, logging, health indicator | bloge-metrics-otel on classpath |
BlogeEventJournalAutoConfiguration | Event journal retention and lifecycle | bloge-event-journal on classpath |
If you're wiring multi-turn session flows, Chapter 14 — Multi-Turn Sessions
explains the runtime model that SessionExecutorAutoConfiguration is bootstrapping:
session graphs, phase/round execution, idle timeouts, and optional durable recovery.
The @BlogeOperator annotation
Defined in BlogeOperator.java:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface BlogeOperator {
@AliasFor(annotation = Component.class, attribute = "value")
String value() default "";
String description() default "";
String owner() default "";
String[] tags() default {};
String promptHint() default "";
String usageExample() default "";
String constraintsDescription() default "";
}
It is @Component underneath, so Spring discovers it. The BlogeAutoConfiguration.operatorRegistry() bean method then scans all beans annotated with @BlogeOperator and registers each one in a DefaultOperatorRegistry keyed by the annotation's value().
For agent/LLM integration, three additional attributes describe the operator to an AI agent:
promptHinttells the agent when to choose this operator;usageExamplegives a DSL snippet showing the operator in context;constraintsDescriptiondocuments constraints not obvious from the schema. All three are optional.
How GraphEngine is assembled
The engine bean is created by BlogeGraphEngineAutoConfiguration, which
accepts tenantContextResolver and tenantResourcePolicy for multi-tenant
deployments and exposes a GraphEngineCustomizer callback hook so you don't
have to replace the entire engine bean to tweak it.
The graphEngine() bean method in BlogeGraphEngineAutoConfiguration uses ObjectProvider for every optional collaborator:
@Bean
@ConditionalOnMissingBean
public GraphEngine graphEngine(
OperatorRegistry registry,
ObjectProvider<OperatorInterceptor> interceptors,
ObjectProvider<ExecutionListener> listeners,
ObjectProvider<CallerContextCarrier> contextCarriers,
ObjectProvider<CheckpointCodec> checkpointCodec,
ObjectProvider<VersionRoutingPolicy> versionRoutingPolicy,
ObjectProvider<TenantContextResolver> tenantContextResolver,
ObjectProvider<TenantResourcePolicy> tenantResourcePolicy,
ObjectProvider<GraphEngineCustomizer> customizers,
/* ... more ObjectProviders ... */) {
GraphEngine.Builder builder = GraphEngine.builder()
.registry(registry)
.interceptors(interceptors.orderedStream().toList())
.listeners(listeners.orderedStream().toList())
.contextCarriers(contextCarriers.orderedStream().toList())
.checkpointCodec(checkpointCodec.getIfAvailable())
.versionRoutingPolicy(versionRoutingPolicy.getIfAvailable())
.tenantContextResolver(tenantContextResolver.getIfAvailable())
.tenantResourcePolicy(tenantResourcePolicy.getIfAvailable());
if (properties.getEngineMode() == BlogeProperties.EngineMode.REQUEST_RESPONSE) {
builder.requestResponseDefaults();
}
customizers.orderedStream().forEach(c -> c.customize(builder));
return builder.build();
}
This means any OperatorInterceptor, ExecutionListener,
CallerContextCarrier, TenantContextResolver, or TenantResourcePolicy you
declare as a Spring bean is automatically picked up by the engine. You never
need to touch the engine builder.
GraphEngineCustomizer is a callback interface that lets you
fine-tune the GraphEngine.Builder after all auto-configured defaults and
engine-mode presets have been applied — without replacing the entire engine bean:
@Bean
public GraphEngineCustomizer myCustomizer() {
return builder -> builder
.schedulerTimerSupport(myTimerSupport);
}
Core configuration properties
Declared in BlogeProperties.java under the spring.bloge prefix:
spring:
bloge:
dsl-locations: classpath:bloge/ # where .bloge files are scanned
hot-reload: false # file-watch and recompile on change
default-timeout: 30s # default node timeout
accessor-mode: method-handle # expression compilation mode
engine-mode: auto # engine preset (auto | request-response)
streaming-default-buffer-size: 16 # streaming buffer capacity
engine-modeandstreaming-default-buffer-size.engine-modeselects a high-level preset for the engine builder.AUTO(the default) uses standard settings;REQUEST_RESPONSEapplies optimised defaults for short-lived, request-scoped graph executions.streaming-default-buffer-sizecontrols the default back-pressure buffer for streaming graph executions.
Tenant properties
When spring.bloge.tenant.enabled=true, the starter creates a
SpringTenantContextResolver and SpringTenantResourcePolicy bean
that feed into the engine's multi-tenant admission control:
spring:
bloge:
tenant:
enabled: false # master switch
mode: header # header | security-context | static
header-name: X-Tenant-Id # HTTP header for tenant resolution
namespace-header-name: X-Namespace # HTTP header for namespace resolution
default-tenant: default # fallback tenant ID
default-namespace: default # fallback namespace
max-concurrent-executions: 100 # per-tenant concurrency cap
max-starts-per-minute: 0 # per-tenant rate limit (0 = unlimited)
The mode property selects how the tenant is resolved:
| Mode | How it resolves the tenant |
|---|---|
HEADER | Reads X-Tenant-Id and X-Namespace from the HTTP request headers |
SECURITY_CONTEXT | Extracts tenantId / namespace from the Spring Security Authentication.getDetails() |
STATIC | Always returns default-tenant / default-namespace |
When tenant support is enabled, the starter also registers a
TenantContextFilter — a servlet filter that binds the resolved
TenantContext to the request thread via TenantContextHolder.callWith(),
so store queries and default GraphContext construction see the same
tenant for the entire HTTP request.
Version-routing properties
spring:
bloge:
version-routing:
policy: latest # latest | pinned | canary
pinned-version: null # required when policy=pinned
canary-primary: null # primary version for canary
canary-candidate: null # candidate version for canary
canary-percentage: 10 # traffic % routed to candidate
Durable routing properties
spring:
bloge:
durable:
routing:
mode: single # single | hash | tenant
tenant-strategy: null # shared-table | separate-schema | separate-database
default-shard-id: default
fallback-shard-id: null
shard-ids: []
tenant-shards: {} # map of tenantId → shardId
Durable store wiring
When bloge-durable is on the classpath and a DataSource bean exists, the
starter creates a durableSqlSessionFactory bean, runs Flyway migrations (if
spring.bloge.durable.flyway.enabled=true, the default), and registers ten+
runtime store beans — each with @ConditionalOnMissingBean so you can override
any of them. The full list includes ExecutionStore,
ExecutionCheckpointStore, WaitStore, WorkItemStore, GraphRegistryStore,
RoutingStore, TaskInboxStore, EventMatcherStore, EventDeduplicationStore,
ArchiveService, and DurableControlPlaneService.
Health indicator
BlogeHealthIndicator
reports UP when at least one graph is loaded and DOWN with
"reason": "No graphs loaded" when the list is empty. It appears at
/actuator/health under the bloge component.
Error Responses — RFC 9457 ProblemDetail
When bloge-spring-web is on the classpath, every BLOGE exception is
mapped to a RFC 9457 ProblemDetail so clients see a uniform
machine-readable error format, not stack traces.
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://bloge.dev/problems/graph-version-mismatch",
"title": "Graph version mismatch",
"status": 422,
"detail": "Execution started against orderProcess@v3 but live registry has @v4.",
"instance": "/executions/exec-12af9",
"executionId": "exec-12af9",
"graphId": "orderProcess",
"expectedHash": "sha256:1f3a…",
"actualHash": "sha256:9c0e…"
}
Built-in mappings (configured by BlogeProblemDetailAdvice):
| Exception | HTTP status | type URI suffix |
|---|---|---|
GraphDefinitionException | 400 | /problems/graph-definition |
GraphValidationException | 422 | /problems/graph-validation |
GraphVersionMismatchException | 422 | /problems/graph-version-mismatch |
SessionAccessDeniedException | 403 | /problems/session-access-denied |
StaleLeaseException | 409 | /problems/stale-lease |
OperatorTimeoutException | 504 | /problems/operator-timeout |
| Anything else | 500 | /problems/internal (no exception detail leaked) |
Disable or override the advice by declaring your own
@RestControllerAdvice bean — the auto-configuration is gated on
@ConditionalOnMissingBean(BlogeProblemDetailAdvice.class).
Guided Rewrite
Start from the ticket-triage example and add production features one layer at a time.
1. Add observability
Add bloge-metrics-otel to your POM. No configuration changes needed — the
starter detects MetricsExecutionListener on the classpath and registers it
with your existing MeterRegistry.
Verify:
GET /actuator/bloge
You should see:
{
"graphCount": 1,
"graphs": [
{
"name": "springTicketTriage",
"nodeCount": 2,
"edgeCount": 1,
"sourceNodes": ["classifyTicket"],
"terminalNodes": ["draftReply"]
}
]
}
2. Enable audit journaling
Add to application.yml:
spring:
bloge:
audit:
enabled: true
capture-input: true
capture-output: false
async-flush: true
flush-interval: 100ms
batch-size: 64
This requires a durable DataSource so that AuditJournalStore is available.
The BlogeAuditAutoConfiguration creates an AuditJournalListener that the
engine picks up automatically.
3. Enable crash recovery
spring:
bloge:
recovery:
enabled: true
scan-interval: 30s
batch-size: 10
claim-timeout: 5m
max-recovery-attempts: 3
The starter creates a RecoveryConfig bean and wires it into GraphEngine. The
engine manages execution leases in the background and scans for expired
RUNNING executions to resume.
Recovery requires
bloge-durableon the classpath and Flyway migrations at version V11 or later.
4. Add a custom interceptor
@Bean
public OperatorInterceptor loggingInterceptor() {
return invocation -> {
log.info("Executing node: {}", invocation.nodeId());
return invocation.proceed();
};
}
The graphEngine() bean collects all OperatorInterceptor beans via
ObjectProvider<OperatorInterceptor> and injects them in Spring ordering. No
registration code needed.
5. Production deployment patterns
Three patterns recur in production BLOGE deployments. None of them require custom engine code — they are configuration and convention.
Graph versioning. The GraphRegistryStore (wired automatically with
bloge-durable) persists each compiled graph by name + content hash. When you
deploy a new .bloge file version, the starter compiles and registers it on
startup. In-flight executions continue on the old graph definition; new
executions use the new one. The starter exposes version-routing
policies via spring.bloge.version-routing.policy:
latest(default) — always use the newest graph definition.pinned— lock to a specific version withpinned-version.canary— split traffic betweencanary-primaryandcanary-candidateat the configuredcanary-percentage.
For example, to route 10 % of executions to a new graph version:
spring:
bloge:
version-routing:
policy: canary
canary-primary: "1.0.0"
canary-candidate: "1.1.0"
canary-percentage: 10
Environment-specific operator bindings. Different environments may need different operator implementations (e.g., a sandbox payment gateway in staging vs. a real one in production). Use Spring profiles:
@BlogeOperator(value = "ChargePaymentOperator")
@Profile("production")
public class RealChargePaymentOperator { … }
@BlogeOperator(value = "ChargePaymentOperator")
@Profile("staging")
public class SandboxChargePaymentOperator { … }
The DSL references ChargePaymentOperator without knowing which
implementation is active. The Spring profile selects the correct bean.
Blue-green deployment with durable graphs. During a rolling update, both
old and new JVM instances run simultaneously. Because GraphEngine is
stateless and execution leases are in the database, this is safe:
- New instances start and begin handling new requests.
- Old instances finish in-flight executions.
- If an old instance is killed before finishing, its lease expires and a new instance recovers the execution.
The only constraint: ensure the new graph version's operator output schemas are backward-compatible with checkpoints written by the old version (see Chapter 13).
Production Readiness Scan
| Boundary | Question to answer before release |
|---|---|
| Component ownership | Which bean is auto-configured, which is overridden, and who owns the override? |
| Operator discovery | Can the application prove every required operator is registered exactly once? |
| Persistence | Are execution, wait, and work-item stores durable where restart recovery is claimed? |
| Routing | Are tenant, version, and durable-routing policies explicit and tested? |
| Error contract | Do clients receive stable ProblemDetail types without internal exception leakage? |
| Observability | Can one execution identity join logs, metrics, traces, and business events? |
| Recovery | Has a cold restart been tested against the configured store and version? |
Record the answer and the evidence location. A property value without an owner, test, and operational consequence is configuration trivia—not readiness.