Dependency Graph
All engine modules depend on ml-inference-core. The SQL module depends on
ml-inference-core and optionally on whichever engine modules are on the runtime
classpath. Flink is always provided scope — it must never be bundled in the shaded JAR.
otter-streams (parent pom) | +-- ml-inference-core <-- InferenceEngine, InferenceResult, ModelCache, metrics | +-- otter-stream-sql <-- depends on: ml-inference-core | +-- otter-stream-onnx <-- depends on: ml-inference-core, onnxruntime +-- otter-stream-tensorflow <-- depends on: ml-inference-core, tensorflow-core-platform +-- otter-streams-xgboost <-- depends on: ml-inference-core, xgboost4j +-- otter-stream-pmml <-- depends on: ml-inference-core, jpmml-evaluator +-- otter-stream-pytorch <-- depends on: ml-inference-core, djl-pytorch-engine +-- otter-stream-remote <-- depends on: ml-inference-core, okhttp, aws-sdk | +-- otter-stream-examples (not published)
ml-inference-core
The foundation of the library. Every other module declares a compile dependency on this artifact. It contains no ML framework code — only the contracts, configuration objects, caching layer, and Flink async function wiring that all engines share.
Key classes
| Class / Interface | Package | Purpose |
|---|---|---|
InferenceEngine<C> | ...inference.engine | Top-level contract all engines implement. Typed on a config record. |
InferenceResult | ...inference.model | Holds output tensors, latency, and success flag. |
InferenceException | ...inference.exception | Checked exception thrown by engine.infer(). |
ModelCache | ...sql.loader | Caffeine-backed LRU singleton keyed by model name. |
InferenceConfig | ...inference.config | Builder for batch size, timeouts, retries, caching, metrics. |
AsyncModelInferenceFunction | ...inference.function | Flink AsyncFunction wrapper for use in DataStream pipelines. |
Runtime layer (com.codedstream.otterstream.runtime) — new in 0.1.0
OtterRuntime is an optional facade that sits above the classes in the table
above. It does not replace them — every existing class continues to work exactly as
before and does not require the runtime to function. What it adds is a Provider SPI
(so engine implementations are discovered rather than hardcoded) and a Lifecycle Manager
(so a model version can be validated, warmed, and atomically swapped in without ever
exposing a partially-initialized engine to callers).
| Class / Interface | Package | Purpose |
|---|---|---|
OtterRuntime | ...runtime | Central facade + builder tying together the provider registry, model registry, lifecycle manager, cache, and metrics. |
InferenceProvider | ...runtime.spi | SPI implemented by each engine module (ONNX, TensorFlow, ...) so it can be discovered via ServiceLoader instead of hardcoded. |
ModelRegistry | ...runtime.spi | SPI for resolving a ModelReference (id + optional version) to a loadable ModelConfig. Extension point for MLflow/S3/Nexus-backed registries. |
ModelReference | ...runtime.spi | Value type: a logical model id, optionally pinned to a version. |
FeatureProvider | ...runtime.spi | SPI shape for feature-store lookups. Interface only for now — implementations (Redis, JDBC, Feast) land in a later milestone. |
ProviderRegistry | ...runtime.registry | Holds registered InferenceProviders, resolves the right one per ModelFormat, supports ServiceLoader auto-discovery. |
DefaultModelRegistry | ...runtime.registry | Simple in-memory ModelRegistry implementation used when no other registry is supplied. |
LifecycleManager | ...runtime.lifecycle | Orchestrates validate → warm → atomic swap → drain → retire for every deployment, plus rollback, canary, and shadow flows. |
ManagedModel | ...runtime.lifecycle | Stable handle for a model id. Routes traffic between primary/canary engines, mirrors sampled traffic to a shadow engine, and tracks in-flight requests per engine for graceful draining. |
ModelVersion | ...runtime.lifecycle | Tracks a single deployment's stage: VALIDATING → WARMING → ACTIVE → RETIRED (or FAILED) — and the ModelConfig it came from, for rollback. |
DynamicLoader | ...runtime.lifecycle | Polls a ModelRegistry per watched model id and auto-deploys newly discovered versions. |
LifecycleListener | ...runtime.lifecycle | Optional observer callback for deployment events (logging, metrics, alerting). |
ShadowListener | ...runtime.lifecycle | Optional observer callback fired with (primary result, shadow result) pairs for comparison/logging. |
DeploymentException | ...inference.exception | Thrown when validation or warmup fails during a deployment. |
OtterRuntime runtime = OtterRuntime.builder()
.metrics(new MetricsCollector())
.build(); // auto-discovers otter-stream-* providers on the classpath via ServiceLoader
ManagedModel model = runtime.deploy(ModelConfig.builder()
.modelId("fraud-detector")
.modelPath("file:///models/fraud_detection.onnx")
.format(ModelFormat.ONNX)
.modelVersion("1.0")
.build());
InferenceResult result = runtime.infer("fraud-detector", inputs);
// later — hot-swap to v2; no code change needed at the call site above.
// the old engine is drained (in-flight requests finish) before it's closed.
runtime.deploy(v2Config);
runtime.close();
Provider coverage: ONNX, TensorFlow, PyTorch, XGBoost, PMML, remote HTTP,
and SageMaker all ship an InferenceProvider and are auto-discovered.
Vertex AI and gRPC remote clients are not yet covered by the SPI — their constructors
require an InferenceConfig at construction time rather than following the
no-arg-constructor-then-initialize(ModelConfig) pattern the rest of the
engines use, so they can't be created generically yet. They remain fully usable directly
(outside OtterRuntime); aligning them with the rest of the SPI is tracked as
a follow-up.
Dynamic model loading & rollback
OtterRuntime.watch(modelId, interval) polls the configured
ModelRegistry on a per-model schedule and deploys new versions automatically
as they're registered — no code needs to call deploy() by hand. Every swap,
whether triggered manually or by the watcher, drains the outgoing engine (waits for its
in-flight requests to finish, up to a configurable timeout) before closing it, so a hot
swap under load never severs an in-progress call. rollback() re-deploys the
last known-good version if a new one turns out to be bad.
runtime.watch("fraud-detector", Duration.ofSeconds(30));
// ... new versions registered in the ModelRegistry are now picked up automatically ...
runtime.unwatch("fraud-detector");
// something's wrong with the latest deploy — go back to the previous version
runtime.rollback("fraud-detector");
Shadow & canary deployments
Canary: deploy a candidate version alongside the current primary at a
chosen traffic percentage; each call to infer() is routed to the canary or
the primary per that split. promoteCanary() makes it the new primary (and
drains/retires the old one); rollbackCanary() discards it, leaving the
primary untouched.
Shadow: deploy a version that receives a sampled, asynchronous copy of
traffic purely for comparison — it never affects what the caller receives, and a
ShadowListener is notified with each (primary, shadow) result pair.
// canary: 10% of traffic to the candidate
runtime.deployCanary(v2Config, 10);
// looks good after watching metrics/logs for a while...
runtime.promoteCanary("fraud-detector");
// ...or if it doesn't:
// runtime.rollbackCanary("fraud-detector");
// shadow: mirror 5% of traffic to a candidate, compare offline, no user-facing impact
runtime.addShadowListener((modelId, shadowVersion, primary, shadow, error) -> {
if (error == null && !primary.equals(shadow)) {
log.warn("Shadow mismatch for {} v{}", modelId, shadowVersion);
}
});
runtime.deployShadow("fraud-detector", candidateConfig, 0.05);
runtime.stopShadow("fraud-detector");
Maven
<dependency> <groupId>com.codedstreams</groupId> <artifactId>ml-inference-core</artifactId> <version>0.0.4</version> </dependency>
Feature store providers
Implementations of the FeatureProvider SPI (runtime.spi.FeatureProvider).
Unlike InferenceProviders, feature providers always need connection details
(a host, a JDBC URL, a base URL) to be useful, so they're constructed directly by your
application rather than auto-discovered — the same reasoning that keeps
ModelCache and MetricsCollector explicit constructor calls
instead of SPI-discovered singletons. OtterRuntime does not yet wire feature
lookups into the inference call path itself; these providers are ready to call directly
from your own pre-processing step.
| Module | Class | Backing store | Notes |
|---|---|---|---|
otter-stream-feature-redis | RedisFeatureProvider | Redis (via Jedis) | Reads a per-entity hash (HGETALL/HMGET); values come back as raw strings. |
otter-stream-feature-jdbc | JdbcFeatureProvider | Any JDBC source | One row per entity, one column per feature. No driver bundled — bring your own (PostgreSQL, MySQL, Snowflake, ...). Accepts a raw JDBC URL or a pooled DataSource. |
otter-stream-feature-feast | FeastFeatureProvider | Feast (HTTP feature server) | Calls POST /get-online-features on a Feast HTTP feature server. Targets REST, not the gRPC serving API. |
RedisFeatureProvider redis = new RedisFeatureProvider("localhost", 6379, "features:user:");
Map<String, Object> values = redis.fetch("42", List.of("age", "country", "ltv_score"));
JdbcFeatureProvider jdbc = new JdbcFeatureProvider(
"jdbc:postgresql://localhost:5432/features", "reader", "secret",
"user_features", "user_id");
Map<String, Object> row = jdbc.fetch("42", List.of("age", "country"));
FeastFeatureProvider feast = new FeastFeatureProvider("http://localhost:6566", "driver_id");
Map<String, Object> feats = feast.fetch("1001",
List.of("driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"));
otter-stream-rules, otter-stream-rules-drools, otter-stream-kafka
Post-inference decision engine and result publishing. Full documentation, including architecture diagrams and the expression language reference, lives on its own page: Rule Engine →
| Module | Purpose | Dependencies |
|---|---|---|
otter-stream-rules | YAML/properties/programmatic rule engine, Decision model, RestDecisionEngineConnector | ml-inference-core + SnakeYAML only |
otter-stream-rules-drools | Optional embedded Drools DecisionEngineConnector | + drools-core, drools-compiler, kie-api — isolated to this module |
otter-stream-kafka | JSON serialization schemas + one-liner KafkaSink builders for InferenceResult/Decision | + flink-connector-kafka (provided scope) |
otter-stream-sql
Provides everything needed to use Otter Streams from Flink SQL without writing Java operators.
The module registers a scalar UDF, a lookup table function, and a full dynamic table connector
(ml-inference) that the Flink planner discovers via Java SPI.
Key classes
| Class | Type | SQL usage |
|---|---|---|
MLInferenceFunction | ScalarFunction | ml_score(features, 'model-name') |
MLInferenceLookupFunction | TableFunction | Temporal join source |
MLInferenceDynamicTableFactory | DynamicTableSourceFactory | 'connector' = 'ml-inference' |
MLInferenceDynamicTableSource | LookupTableSource | Lookup join runtime provider |
SqlInferenceConfig | Config record | Parses DDL WITH options |
*-flink-udf classifier artifact so that ml-inference-core and all
transitive runtime deps are bundled. See the Shaded JAR Build section.Maven
<dependency> <groupId>com.codedstreams</groupId> <artifactId>otter-stream-sql</artifactId> <version>0.0.4</version> </dependency>
otter-stream-onnx
Wraps Microsoft's ONNX Runtime Java API. Supports CPU, CUDA, and TensorRT execution providers
with automatic provider fallback. Input tensors are constructed from Map<String, Object>
with automatic float32 / int64 conversion.
Supported execution providers
| Provider | Hardware | Requires |
|---|---|---|
CPUExecutionProvider | Any CPU | Default — no extra dependencies |
CUDAExecutionProvider | NVIDIA GPU | CUDA 11.x + cuDNN |
TensorRTExecutionProvider | NVIDIA GPU | TensorRT 8+ |
Maven
<dependency> <groupId>com.codedstreams</groupId> <artifactId>otter-stream-onnx</artifactId> <version>0.0.4</version> </dependency>
otter-stream-tensorflow
Loads TensorFlow SavedModel directories and performs inference via the official TensorFlow Java API.
Signature names and tensor names are discovered automatically from the model's saved_model.pb.
tensorflow-core-platform includes native shared libraries (~400 MB per platform).
Use the -linux-x86_64 or -linux-gpu-x86_64 classifiers in production
to avoid bundling all platforms in the shaded JAR.Maven
<dependency> <groupId>com.codedstreams</groupId> <artifactId>otter-stream-tensorflow</artifactId> <version>0.0.4</version> </dependency>
otter-streams-xgboost
Loads XGBoost models using XGBoost4J. Converts feature maps to a DMatrix and invokes
Booster.predict(). Thread-safe via per-call DMatrix construction. Supports binary
classification (binary:logistic), multi-class, and regression objectives.
Supported file formats
| Format | Extension | Notes |
|---|---|---|
| Binary | .bin, .model | Fastest load time; not human-readable |
| JSON | .json | Portable; readable; XGBoost ≥ 1.0 |
| UBJSON | .ubj | Compact binary JSON; XGBoost ≥ 1.7 |
Maven
<dependency> <groupId>com.codedstreams</groupId> <artifactId>otter-streams-xgboost</artifactId> <version>0.0.4</version> </dependency>
otter-stream-pmml
Evaluates PMML 4.x documents using the JPMML-Evaluator library. Supports logistic regression, decision trees, random forests, gradient boosted trees, neural networks, and naïve Bayes. Preprocessing transformations defined inside the PMML document are applied automatically.
Maven
<dependency> <groupId>com.codedstreams</groupId> <artifactId>otter-stream-pmml</artifactId> <version>0.0.4</version> </dependency>
otter-stream-remote
Routes inference calls to external endpoints: REST APIs, AWS SageMaker, GCP Vertex AI, and Azure ML. Uses OkHttp for HTTP/1.1 and gRPC for binary protocol communication. Includes configurable retry, timeout, and circuit-breaker policies. Useful when the model is too large to embed in the Flink JAR or when a dedicated model server is preferred.
Maven
<dependency> <groupId>com.codedstreams</groupId> <artifactId>otter-stream-remote</artifactId> <version>0.0.4</version> </dependency>