Show HN: Itara – Distributed system topology as an explicit, executable layer
Itara is an open-source project that makes distributed system topology explicit by separating it into a dedicated configuration layer. It uses a wiring agent that reads a config file at startup, resolves all connections, and wires components together before the application runs at full speed. The tooling validates topologies before deployment and provides observability through four key events. It supports incremental adoption and cross-language interop (Java, Rust, and more planned).
Uh oh!
There was an error while loading. Please reload this page.
Notifications You must be signed in to change notification settings
Fork 1
Star 10
BranchesTags
Open more actions menu
Folders and files
NameName
Last commit message
Last commit date
Latest commit
History
64 Commits
64 Commits
.github
.github
demo
demo
docs
docs
java
java
rust
rust
spec
spec
.gitattributes
.gitattributes
.gitignore
.gitignore
CONTRIBUTING.md
CONTRIBUTING.md
GLOSSARY.md
GLOSSARY.md
LICENSE
LICENSE
README.md
README.md
Repository files navigation
Itara makes distributed system topology explicit, declared, verifiable, and executable. Topology — which components exist, how they connect, what transport they use, how failures are handled — lives in a single wiring config, not scattered across codebases as a consequence of implementation decisions made years apart.
This is achieved through two co-equal parts: a language-specific wiring agent that reads the wiring config before the application starts, resolves all connections once, wires the components together, and then steps aside — the application runs at full speed with no intermediary, no proxy in the call path, no decisions made at call time — and a tooling ecosystem that makes the topology layer safe and manageable. The tooling validates configurations before deployment, catches mismatches and incompatibilities at authoring time, visualises the topology as a graph, and guides engineers through changes. Incorrect topologies cannot be deployed silently. The layer Itara introduces is the layer the tooling understands completely.
Change how your components communicate — collocated direct calls, HTTP, message queues — by changing a config file. No code changes. No redeployment ceremony. No migration scripts. Reference implementations of the wiring agent exist in Java and Rust. More languages are planned. The tooling ecosystem has begun: itara-cli ships two commands — itara inspect to visualise a topology and itara verify to catch configuration errors before deployment. The visual editor and controller are planned.
Contents
The problem
The idea
Gradual adoption
The proof of concept
Language support
Observability
Event-driven topology
Failure semantics
Repository structure
The demo
Running the Rust demo
The CLI
What Itara is not
Current state
The problem
The pain
Changing how components in a distributed system communicate is painful and dangerous — it means changing code on both sides, finding all the clients that call a particular service, identifying the relevant code owners and teams, making a step-by-step plan, and coordinating the effort across teams. Even in the most well-designed systems, it is a very significant and time-consuming effort. Most of us have been there: mark an API deprecated, replace it everywhere you know it's called, and then spend a week watching the monitoring — hoping nothing you did not know about would call it. When nothing does, you delete it and move on, never quite sure whether you were thorough enough or just got lucky. There are ways to mitigate the risk: parallel runs, rollback plans, coordinated change freezes, additional observability during migration, the strangler fig pattern — we do them routinely. But none of them tackle the real issue. They only mitigate the consequences.
The root cause
The topology of a distributed system — which components exist, how they communicate, where they run — is not a first-class concern. It lives in HTTP clients, retry policies, timeout configurations, service discovery calls, Kafka consumer group registrations, schema registry entries: scattered across every service, encoded as a consequence of implementation decisions made years apart by people who may no longer be around. There is no place you can go to ask what the topology is right now, what depends on what, or what breaks if you move something. The blast radius of any topology change is very hard to determine, often impossible, until you are already committed to it.
See VISION.md for the full analysis and MANIFESTO.md for the principles that follow from it.
The idea
We've solved this pattern before — with configuration, with infrastructure as code. Distributed system topology is next.
Topology belongs in a dedicated, separately declared layer — not distributed across the codebase as a consequence of implementation decisions. Itara elevates and concentrates it there.
A component declares its contract — what it accepts and what it returns. It does not declare how it is called — that is declared in the wiring config. The wiring config is not a deployment detail. It is the authoritative representation of the system's topology: which components exist, how they connect, what transport they use, how failures are handled, expressed as a directed graph.
The Itara wiring agent reads the config at startup, prepares all connections exactly as declared, and then steps aside. No decisions are made at call time. The topology is fully determined before the first request arrives.
Every artifact the system must reason about — components, API contracts, transport implementations, serializers, observers, event contracts — declares itself via a .itara metadata file. This is the symbol table the agent and tooling work from: identifiers, versions, declared contracts, capability declarations, and idempotency flags all live there. The tooling reads these files to validate compatibility and catch misconfigurations before deployment.
This is not about hiding the network or making remote calls look like local ones. Topology is explicit, not hidden — declared in the wiring config, validated by tooling before deployment, and visible to everyone. See the FAQ for how Itara compares to service meshes, sidecars, and transparency-oriented systems. See SPEC.md for the formal specification.
The wiring config for a two-component system looks like this:
One master config describes the entire topology.
Each process is told which node it represents at startup.
Change this file. Restart. Topology changes.
nodes:
- id: gatewayNode
component: gateway
- id: calculatorNode
component: calculator
connections:
- from: gatewayNode
to: calculatorNode transport: type: direct # or: http, kafka — code does not change
Gradual adoption
Itara is designed to be adopted incrementally — one component at a time, one service boundary at a time, without touching the rest of the system.
A component needs three things to participate:
An API artifact — a plain interface declaring the contract
An activator — a single factory method that receives the registry and returns the implementation
A wiring config entry — declaring the node and its connections
The only Itara dependency a component ever introduces is the core library — itara-common in Java, itara-core in Rust. The business logic implementation itself requires no Itara imports at all. Only the activator, the single composition root of a component, touches the registry. Spring Boot works alongside Itara without any adapter — they operate at different layers and do not interfere with each other.
With partial adoption, even a single service boundary under Itara gives you structural observability on that connection, explicit topology declaration for those components, and the ability to switch transport without code changes.
With full adoption, the complete topology picture becomes available. The wiring config describes the entire system. The CLI derives all deployment groups, validates all connections, and detects compatibility issues before deployment. The blast radius of any topology change becomes determinable rather than estimated.
The full benefits require the full picture. The value of partial adoption is real and immediate.
The proof of concept
Two components — a gateway and a calculator — implemented in both Java and Rust. Switch between direct and HTTP topology by changing one line in the wiring config. The component code does not change.
Direct topology — single process, zero network overhead:
[Gateway] add(3, 4) = 7
HTTP topology — separate processes, transport cost visible in the traces:
[Itara/HTTP] -> add on calculator at localhost:8081 [Gateway] add(3, 4) = 7
This works across languages. A Java gateway calling a Rust calculator produces a single distributed trace in Kibana — the same trace ID, correct parent-child relationships, across two runtimes. The code in either component does not change.
The order processing demo below shows the full picture: four Java components, one Rust service, three topologies, and the traces that make each topology decision measurable.
Language support
Language Status Notes
Java Reference implementation JVM agent, full observability, Spring Boot compatible
Rust Working implementation Transport SPI, config parser, agent library
Go Planned —
Python Planned —
C++ Planned —
Components in different languages participate in the same topology graph and produce the same distributed traces.
Observability
Itara treats observability as a first-class citizen. Every component call produces four events regardless of transport:
Event Side Fires
CALL_SENT Caller proxy When the business layer hands over the call to the topology layer
CALL_RECEIVED Callee dispatcher When the topology layer hands over the call to the business layer
RETURN_SENT Callee dispatcher When the business layer hands over the return to the topology layer
RETURN_RECEIVED Caller proxy When the topology layer hands over the return to the business layer
The placement is deliberate. The outer span (CALL_SENT → RETURN_RECEIVED) measures the full round trip from the caller's perspective. The inner span (CALL_RECEIVED → RETURN_SENT) measures pure component processing time, independent of transport. The gap between them is the transport overhead — serialization cost, network latency, deserialization cost — directly measurable without a single line of monitoring code. This is possible because the topology layer and the business layer have a clear boundary — and that boundary is precisely where these events fire.
Switching a connection from direct to HTTP changes the latency numbers in the traces. Nothing else changes. Topology decisions have a measurable cost, visible before you commit to them.
Beyond the four key events, plugins — including failure semantics implementations — may emit custom spans. These appear as children of the active context in the trace, making sub-call events such as retry attempts observable without modifying the four-event model.
The observer SPI is pluggable — any implementation can consume the four events and the custom spans. An OpenTelemetry observer ships with the open core as the reference implementation: distributed traces appear in Kibana with correct parent-child relationships across JVMs, using W3C traceparent headers for propagation. The Rust implementation has working OTel support — including cross-language traces spanning Java and Rust processes — at proof of concept level, planned for a redesign before production use.
See ADR 0003 for the full observability model, ADR 0010 for why the events fire where they do, ADR 0013 for the Rust implementation state, and ADR 0014 for the correlation ID model.
Event-driven topology
Itara supports event-driven communication through virtual nodes and event contracts. A virtual node represents an event channel — a Kafka topic, a queue, or any async transport — declared in the wiring config like any other node. The four-event observability model extends naturally to async calls: the producer side fires CALL_SENT and RETURN_RECEIVED, the consumer side fires CALL_RECEIVED and RETURN_SENT. The itaraTraceId correlation key links the two sides in the trace.
Event contracts are declared in API artifacts alongside regular method contracts. A component declare
[truncated for AI cost control]