The Telamin Stack¶
A deterministic event-processing stack. The processor is a compiled artifact — not a runtime interpretation of a topology. Audit, replay, testing, debugging, and LLM authoring fall out of that single design choice.
Why this exists¶
Traditional event-processing stacks force you to pick two of three:
- Speed — raw threads and lock-free queues, but no audit trail and you can't reproduce a production bug.
- Safety — heavyweight cluster framework with snapshots and exactly-once, but the code path is opaque and the dependencies are vast.
- Flexibility — orchestrator-of-orchestrators (Camel, Spring Integration), but you give up performance and deterministic ordering.
Telamin's premise: compile the event processor, and you get all three.
flowchart LR
classDef trad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef tel fill:#dcfce7,stroke:#16a34a,color:#14532d
subgraph T[Traditional stream framework]
direction TB
T1[Topology DSL]:::trad --> T2[Runtime interprets graph]:::trad
T2 --> T3[Reflective dispatch]:::trad
T3 --> T4[Probabilistic order under load]:::trad
T4 --> T5[Logs you hope to debug]:::trad
end
subgraph M[Telamin]
direction TB
M1[Plain Java / DSL / Spring XML / YAML / LLM]:::tel --> M2[Fluxtion compiler]:::tel
M2 --> M3[Generated Java + GraphML]:::tel
M3 --> M4[Deterministic dispatch at runtime]:::tel
M4 --> M5[Replayable audit log + readable artifact]:::tel
end
The rest of this page walks through the stack layer by layer. Each layer states what you get and what traditional systems do instead.
The full stack at a glance¶
flowchart TB
classDef authoring fill:#fef3c7,stroke:#d97706,color:#78350f
classDef compile fill:#e0e7ff,stroke:#4338ca,color:#312e81
classDef artifact fill:#fae8ff,stroke:#a21caf,color:#701a75
classDef runtime fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef plugins fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef tools fill:#ffe4e6,stroke:#e11d48,color:#881337
A1[Plain Java POJO graph]:::authoring
A2[DSL builder]:::authoring
A3[Spring XML]:::authoring
A4[YAML]:::authoring
A5[LLM-emitted bean defs]:::authoring
C[Fluxtion compiler]:::compile
R1[Generated .java]:::artifact
R2[GraphML diagram]:::artifact
R3[.class file]:::artifact
M[Mongoose server<br/>agent-hosted dispatcher]:::runtime
P1[Connectors<br/>aeron / chronicle / file / kafka / multicast]:::plugins
P2[Services<br/>cache / jdbc / admin-rest / admin-telnet / admin-web / loaders]:::plugins
P3[Libraries<br/>lib-jsonserialiser]:::plugins
T[mongoose-test-support<br/>+ examples + replay]:::tools
A1 --> C
A2 --> C
A3 --> C
A4 --> C
A5 --> C
C --> R1
C --> R2
C --> R3
R3 --> M
P1 --> M
P2 --> M
P3 --> M
M --> T
R1 --> T
Six layers, each with a clear job. Each layer is also independently useful — you can stop at Layer 3 if you only need the compiled processor.
Layer 1 — Authoring¶
flowchart LR
classDef in fill:#fef3c7,stroke:#d97706
classDef out fill:#e0e7ff,stroke:#4338ca
J[Plain Java POJOs<br/>@OnTrigger, @ServiceRegistered]:::in
D[DSL graph builder<br/>FlowBuilder]:::in
X[Spring XML<br/>bean definitions]:::in
Y[YAML<br/>config]:::in
L[LLM-emitted<br/>bean defs]:::in
F[Fluxtion compiler]:::out
J --> F
D --> F
X --> F
Y --> F
L --> F
The Fluxtion compiler accepts a logical model of the graph in multiple
authoring shapes. Plain Java POJOs are the most common form — annotate
methods with @OnTrigger, declare dependencies by field reference, and
the compiler infers the dispatch graph. The same compiler also accepts
DSL builders, Spring XML bean definitions, YAML config, and any other
shape that emits compatible bean definitions — including from an LLM.
What you get¶
- Authoring decoupled from runtime. The same processor can be authored five different ways. The runtime doesn't know or care.
- No framework inheritance required. Your nodes are plain POJOs. Test them with plain JUnit, no special harness for unit-level tests.
- External authoring tools are first-class. Spring XML is a carrier for any GUI / drag-and-drop / regulated-industry tool that emits bean definitions. The LLM authoring pattern (patented, provisional) rides on this same surface.
What traditional systems do instead¶
- Frameworks force a specific DSL. Kafka Streams = Kafka Streams DSL. Flink = Flink job graph. Akka = actor protocol.
- Authoring couples to runtime. The topology only makes sense inside the framework's cluster.
- No notion of "same graph, different authoring shape." Migrating authoring tooling means rewriting the topology.
Layer 2 — Compilation¶
flowchart LR
classDef in fill:#fef3c7,stroke:#d97706
classDef proc fill:#e0e7ff,stroke:#4338ca
classDef out fill:#fae8ff,stroke:#a21caf
BD[Bean definitions<br/>+ trigger annotations]:::in
subgraph FC[Fluxtion compiler]
direction TB
S1[Topology<br/>analysis]:::proc
S2[Dispatch order<br/>computed]:::proc
S3[Source<br/>generation]:::proc
S1 --> S2 --> S3
end
J[Java source]:::out
G[GraphML]:::out
C[.class file]:::out
BD --> FC
FC --> J
FC --> G
FC --> C
The compiler does what compilers do — analyse the graph for cycles and dependencies, compute the dispatch order at compile time, and emit artifacts. The dispatch order is a fixed property of the compiled output.
What you get¶
- Deterministic dispatch order. Two events that arrive in order A→B always trigger the handler chain in the same order. No runtime polymorphism, no hash-table-iteration luck.
- A diagram of the actual code path. GraphML is generated from the same analysis that generates the dispatch — it can't drift from the code, because it is the code.
- A Java artifact, not a runtime config. No "topology loader" inside the JVM — the topology IS the JVM code.
What traditional systems do instead¶
- Reflective dispatch at runtime. Each event triggers a per-event decision: which subscribers? In what order?
- Diagram drift. Architecture diagrams in Confluence stop matching the code two sprints in.
- Topology lifted from config at startup. Bugs in the topology only surface after the cluster boots — and only on the production data path.
Layer 3 — Runtime artifact¶
flowchart LR
classDef art fill:#fae8ff,stroke:#a21caf
classDef use fill:#dbeafe,stroke:#2563eb
J[Generated .java]:::art
G[GraphML]:::art
C[.class file]:::art
IDE[IDE breakpoints<br/>step debugger]:::use
GRAPH[Graph viewer<br/>yEd, Cytoscape]:::use
PROD[Production JVM<br/>no framework needed]:::use
J --> IDE
G --> GRAPH
C --> PROD
The output of compilation is three artifacts: a Java source file
(readable), a GraphML file (visualisable), and a compiled .class file
(runnable). They're the same model in three shapes.
What you get¶
- Audit-by-construction. Every event has a single deterministic causal chain through the graph. You don't have to log it — the dispatch order is a fixed property of the artifact.
- Replayable. Record the events that arrived on the input feeds. Re-feed the recording to the same artifact → identical processor state. Bit-exact, every time.
- Debuggable with real tools. Open the generated
.javain IntelliJ. Set a breakpoint on the dispatch line. Step through. No framework code to wade through — the entry point is the next call. - No framework lock-in. The compiled artifact runs in any JVM. You could run it in a Lambda. You could embed it in a desktop app.
What traditional systems do instead¶
- Audit = parse logs and guess. "Why did this event lead to that state change?" requires correlating across distributed logs and hoping the timestamps make sense.
- Replay = cluster snapshot + prayer. Flink savepoints are real but expensive and they don't give you why, only what.
- Debugging = read framework code. Step through Spring's
DispatcherServletor Akka's actor mailbox to find your code. Mostly not your code.
Layer 4 — Mongoose server¶
flowchart TB
classDef in fill:#fef3c7,stroke:#d97706,stroke-width:1.5px
classDef disp fill:#dbeafe,stroke:#2563eb,stroke-width:1.5px
classDef out fill:#dcfce7,stroke:#16a34a,stroke-width:1.5px
classDef svc fill:#ddd6fe,stroke:#7c3aed,stroke-width:1.5px
F1[Source connector]:::in
F2[Source connector]:::in
Q1[Queue]:::disp
Q2[Queue]:::disp
P["Compiled processor
single-thread
deterministic dispatch"]:::disp
S1[Cache service]:::svc
S2[JDBC service]:::svc
SK1[Sink]:::out
SK2[Sink]:::out
F1 -- agent thread --> Q1
F2 -- agent thread --> Q2
Q1 --> P
Q2 --> P
P --- S1
P --- S2
P --> SK1
P --> SK2
Mongoose is the production wrapper around a compiled Fluxtion
processor. It hosts the dispatcher on an Agrona AgentRunner, provides
single-writer sink semantics, and exposes the @ServiceRegistered
injection point for cross-cutting services.
What you get¶
- Same processor across environments. Unit test → integration test → simulation → production. The processor doesn't change; only the deployment knob (which connectors are plugged in) does.
- A small lifecycle contract. Four hooks:
init,start,doWork,tearDown. Clear semantics for each. No 7-stageApplicationContextceremony. - Single-writer sinks by contract. The dispatcher serialises
sendToSinkcalls. Plugin authors don't need locks in user code. - Agent-per-component threading. One agent thread per source / sink / service. Easy to reason about, easy to size.
What traditional systems do instead¶
- Coupling. Kafka Streams ↔ Kafka. Flink topology ↔ Flink cluster. "Move to a different runtime" = rewrite.
- Heavyweight lifecycle. Spring Boot has 7+ overlapping lifecycle hooks. Quarkus has dev-mode-only behaviours. Production looks different from your IDE.
- Threading is opaque. You don't know which thread runs which
handler; you find out when you get a
ConcurrentModificationExceptionin production.
Layer 5 — Plugins¶
flowchart TB
classDef cat fill:#fef3c7,stroke:#d97706
subgraph CON[Connectors]
direction LR
C1[connector-aeron]:::cat
C2[connector-chronicle]:::cat
C3[connector-file]:::cat
C4[connector-kafka]:::cat
C5[connector-multicast]:::cat
end
subgraph SVC[Services]
direction LR
S1[svc-cache]:::cat
S2[svc-jdbc]:::cat
S3[svc-admin-rest]:::cat
S4[svc-admin-telnet]:::cat
S5[svc-admin-web]:::cat
S6[svc-loader-yaml]:::cat
S7[svc-loader-spring]:::cat
end
subgraph LIB[Libraries]
direction LR
L1[lib-jsonserialiser]:::cat
L2[mongoose-test-support]:::cat
end
The plugins are independent Maven modules. Pick the ones you need.
Each pulls in only its own dependencies — connector-file is plain JDK,
connector-aeron brings Aeron 1.48, svc-jdbc brings HikariCP. No
bundled monolith.
Authoring your own — plain Java, no specialised skills¶
Writing a plugin is one of the smallest surfaces in the stack. For a new event source:
public class MyEventSource extends AbstractAgentHostedEventSourceService<MyEvent> {
public MyEventSource() { super("my-source"); }
@Override public void onStart() { /* open external resource */ }
@Override public int doWork() {
// poll external system, publish events via `output.publish(...)`
return eventsPublished;
}
@Override public void tearDown() { /* close resource */ }
}
For a new sink:
public class MySink extends AbstractMessageSink<MyEvent> implements Lifecycle {
@Override public void init() { /* open */ }
@Override protected void sendToSink(MyEvent value) { /* write */ }
@Override public void tearDown() { /* close */ }
}
That's it. No annotation processor to run. No special build step. No framework SPI to register. Drop the jar on the classpath, reference the class in YAML or Java config, and it boots.
What you get¶
- One Maven module per plugin. No transitive bloat — your final jar is your code + the few deps you actually pulled in.
- Plain Java authoring. Extend one abstract class, override two or three methods, ship. Five-minute task.
- Independent versioning. Update
connector-kafkawithout touchingsvc-cache. - A published catalogue. Every plugin has a docs page with config table, sample YAML, and a link to a runnable example.
What traditional systems do instead¶
- Spring Boot starters pull 50 transitive deps.
spring-boot-starter-data-jpaalone brings Hibernate, HikariCP, Spring Data, Spring ORM, JTA — even if you only wanted aConnection. - Quarkus extensions are buildtime compiler plugins. Authoring one feels like writing a small compiler. Not weekend work.
- "Plugin" often means framework boilerplate + your code + a build step. Mongoose plugins are POJOs.
Layer 6 — Developer workflow¶
flowchart LR
classDef step fill:#dbeafe,stroke:#2563eb
W[Write processor<br/>plain Java]:::step
C[mvn package<br/>Fluxtion compiles graph]:::step
T[Test<br/>mongoose-test-support harness]:::step
R[Run<br/>MongooseServer.bootServer]:::step
D[Debug<br/>open generated .java<br/>set breakpoint]:::step
P[Replay<br/>re-feed captured log]:::step
W --> C --> T --> R
R -.bug.-> D
R -.bug.-> P
Day-1 productivity is the goal. Each step is plain Java tooling. No new IDE, no new build system, no new test runner.
What you get¶
- 5-line integration tests.
MongooseTestHarnessboots a real server, awaits a condition, closes cleanly. No TestContainers, no Docker, no cluster boot. - Debugger steps into your code. The generated
.javais the entry point. Set a breakpoint indispatchEventand step through. - Reproducible bugs. Capture the input event log in production
(use
connector-fileorconnector-chronicleas a tap sink). Re-feed it locally → identical processor state → identical bug. - Plain Maven, plain JUnit. No
@SpringBootTest, no@QuarkusTest, no@MicronautTest. JUnit 5 + the harness, that's it.
What traditional systems do instead¶
- Integration tests boot 30-second containers.
@SpringBootTestbrings the whole context up. Quarkus dev-mode helps but only inside Quarkus. - Bug reproduction = restore production state. Flink: replay from a savepoint and hope nothing's diverged. Akka: god help you.
- Debugger lands in framework code. Step through
DispatcherServletfor ten frames before you reach your handler.
LLM-friendly architecture¶
flowchart LR
classDef llm fill:#fef3c7,stroke:#d97706
classDef compile fill:#e0e7ff,stroke:#4338ca
classDef det fill:#dcfce7,stroke:#16a34a
H[Human prompt<br/>'compliance rule:<br/>flag trades over X']:::llm
L[LLM<br/>probabilistic]:::llm
B[Spring XML / YAML<br/>bean definitions]:::llm
F[Fluxtion compiler<br/>enforces invariants]:::compile
A[Deterministic<br/>compiled artifact]:::det
AU[Audit-by-construction]:::det
RE[Replayable]:::det
H --> L --> B --> F
F --> A
A --> AU
A --> RE
The Fluxtion compiler accepts logical-model input — Spring XML, YAML, or any bean-definition shape. An LLM emits intent-shaped XML or YAML; the compiler enforces determinism on top.
The LLM does what LLMs do: synthesise from intent. The compiler does what compilers do: enforce invariants. The combination ships an event processor that's auditable and replayable even though it was authored by a non-deterministic model.
This pattern is the subject of a provisional patent (12-month conversion window). It positions Telamin uniquely for regulated-industry agentic AI workflows where the determinism guarantee matters and a raw LLM call doesn't cut it.
What you get¶
- LLM authoring without losing determinism. The output is still a deterministic compiled artifact, even though the author was an LLM.
- Audit trail starts at the prompt. Human prompt → LLM bean defs → compiled artifact → event log. Every step is recorded and reproducible.
- Same compiler regardless of authoring tool. An LLM, a drag-and-drop GUI, and a human writing XML all hit the same downstream guarantee.
What traditional systems do instead¶
- LLM calls embedded as runtime steps. The non-determinism leaks into every event's processing. No replay. No audit. Bad for regulated work.
- No bridge between intent-shaped authoring and deterministic execution. You either trust the LLM or rewrite by hand.
Performance¶
The architecture removes the usual sources of dispatch overhead.
What's not there (on the hot path)¶
- No reflection. Dispatch is a fixed-shape Java method call. The JIT inlines it.
- No virtual dispatch through framework interfaces. The compiled artifact knows the exact concrete types.
- No per-event lambda allocation. This used to be a real bug — fixed
in mongoose 1.0.8 (
DeadWheelScheduler.onTimerExpiryHandlercached as afinalfield). The pattern is enforced repo-wide now. - No locks in user code. Sinks are single-writer by the dispatcher contract. Services are single-thread by agent host.
- No JSON parsing on the dispatch path. Wire format choice is the
user's — use
connector-chroniclefor binary, single-host throughput;connector-aeronfor sub-microsecond IPC.
What is there¶
- Agrona AgentRunner. Same LMAX-era machinery that powers Aeron. Battle-tested, well-understood.
- Single-writer queues. Per-source, no contention.
- Compile-time dispatch. One virtual call per
@OnTrigger. Period. - Configurable idle strategies.
SleepingMillisIdleStrategy(1)is the recommended default — sub-millisecond tick latency, well-behaved CPU usage.BusySpinIdleStrategyis available for trading paths.
Honest about benchmarks¶
There's no published benchmark suite yet. The architecture eliminates the usual overhead sources; throughput numbers will land with the next release. What we can say today:
- Aeron IPC round-trip in the integration tests: ~10 ms wall-clock including embedded driver boot. Steady-state per-event is sub-µs.
- File source tail + handler + file sink: ~50 µs per event on unoptimised laptop hardware (driven by file I/O, not dispatch).
- Cache lookup + handler: ~100 ns inclusive (single virtual call).
These are not benchmarks. They're the timings the integration tests happen to land on. Real numbers will come.
What traditional systems pay¶
- Stream frameworks: 10-100× dispatch overhead. Reflection, serialisation, queue handoff between operator threads.
- Spring Boot: full ApplicationContext on the request path. Hundreds of nanoseconds per dispatch even after warm-up.
- Akka: actor mailbox dispatch + serialisation. Designed for resilience, not latency.
What stays constant across the stack¶
flowchart TB
classDef stable fill:#dcfce7,stroke:#16a34a
P[Compiled processor]:::stable
UT[Unit test<br/>in-memory feeds + sinks]:::stable
IT[Integration test<br/>mongoose-test-support]:::stable
SIM[Simulation<br/>file connectors + replay]:::stable
PROD[Production<br/>Aeron + Kafka + Chronicle]:::stable
P --> UT
P --> IT
P --> SIM
P --> PROD
The processor is the constant. The plugins are the variable.
- Same
@OnTriggermethod runs in a unit test, an integration test, a simulation, and production. - The processor doesn't know which it's in. The plugin set decides.
- No "test config" vs "prod config" duality at the processor level.
This is the property that makes Telamin viable for regulated industries: the thing under audit is the same code that ran in test, simulation, and production. There's no migration step where determinism could be lost.
Honest about the gaps¶
The architecture is solid. The peripheral toolchain is still maturing.
- Test harness shipped this week. The
mongoose-test-supportmodule cuts integration-test boot ceremony to five lines. Brand new. - Metrics aren't standardised. Each plugin exposes
getXxxCount()ad-hoc. Micrometer / OpenTelemetry wiring is on the roadmap. - YAML config has no schema. A typo in a property name silently does nothing. Schema generation is plumbing-grade work that hasn't happened yet.
- Native-image (GraalVM) hasn't been tested. The architecture should support it — the compiled artifact is plain Java with no reflective dispatch on the hot path — but it's untried.
- Three Java package namespaces (legacy
com.fluxtion.dataflow.*, legacycom.fluxtion.server.*, canonicalcom.telamin.mongoose.*) are mid-consolidation. See the migration plan.
These are catchable in weeks. The dispatcher core — the part that's hard to get right — is shipped.
Where to go next¶
| Goal | Read this |
|---|---|
| See it run in 5 minutes | Getting started |
| Browse the plugin catalogue | Connectors / Services |
| Write an integration test that actually works | Test harness |
| Learn the threading + lifecycle internals | Architecture deep-dive |
| Production checklist | Operational guide |
| What's stable vs in-flight | Migration & namespaces |
| Runnable demos | Examples |
The dispatcher is the moat. The rest is good engineering.