Skip to main content

Online edition for BLOGE 0.9.9-RC1 · prepared 2026-11-02 · 中文

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 classWhat it providesGate condition
BlogeAutoConfigurationEntry point — imports BlogeCoreAutoConfigurationAlways active
BlogeCoreAutoConfigurationOperatorRegistry, GraphLoader, List<Graph>, tenant resolver, tenant filterAlways active
BlogeDurableMigrationConfigurationExplicit encrypted-checkpoint migration toolingbloge.decrypt-checkpoints=true + migration classes on classpath
BlogeGraphEngineAutoConfigurationGraphEngine via builder with all ObjectProvider collaboratorsActive unless checkpoint decryption migration mode is enabled
BlogeDurableAutoConfigurationAll durable stores, RecoveryConfig, RoutingStore, Flyway migrationsbloge-durable on classpath + DataSource bean
BlogeAuditAutoConfigurationAuditJournalListenerspring.bloge.audit.enabled=true + AuditJournalStore bean present
SessionExecutorAutoConfigurationPure SessionExecutor; optional durable session adapter and managerbloge-session-ext on classpath; durable beans require durable classes plus stores and definition lookup
BlogeStateMachineAutoConfigurationDurable state-machine manager and recovery wiringdurable state-machine class on classpath + spring.bloge.state-machine.enabled=true
BlogeEndpointAutoConfiguration/actuator/bloge, /actuator/blogeStatsspring-boot-actuator on classpath
BlogeObservabilityAutoConfigurationMetrics, tracing, MDC, logging, health indicatorbloge-metrics-otel on classpath
BlogeEventJournalAutoConfigurationEvent journal retention and lifecyclebloge-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: promptHint tells the agent when to choose this operator; usageExample gives a DSL snippet showing the operator in context; constraintsDescription documents 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-mode and streaming-default-buffer-size. engine-mode selects a high-level preset for the engine builder. AUTO (the default) uses standard settings; REQUEST_RESPONSE applies optimised defaults for short-lived, request-scoped graph executions. streaming-default-buffer-size controls 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:

ModeHow it resolves the tenant
HEADERReads X-Tenant-Id and X-Namespace from the HTTP request headers
SECURITY_CONTEXTExtracts tenantId / namespace from the Spring Security Authentication.getDetails()
STATICAlways 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):

ExceptionHTTP statustype URI suffix
GraphDefinitionException400/problems/graph-definition
GraphValidationException422/problems/graph-validation
GraphVersionMismatchException422/problems/graph-version-mismatch
SessionAccessDeniedException403/problems/session-access-denied
StaleLeaseException409/problems/stale-lease
OperatorTimeoutException504/problems/operator-timeout
Anything else500/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-durable on 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 with pinned-version.
  • canary — split traffic between canary-primary and canary-candidate at the configured canary-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:

  1. New instances start and begin handling new requests.
  2. Old instances finish in-flight executions.
  3. 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​

BoundaryQuestion to answer before release
Component ownershipWhich bean is auto-configured, which is overridden, and who owns the override?
Operator discoveryCan the application prove every required operator is registered exactly once?
PersistenceAre execution, wait, and work-item stores durable where restart recovery is claimed?
RoutingAre tenant, version, and durable-routing policies explicit and tested?
Error contractDo clients receive stable ProblemDetail types without internal exception leakage?
ObservabilityCan one execution identity join logs, metrics, traces, and business events?
RecoveryHas 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.