Skip to main content

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 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.