This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Explanation

Why Itara is built the way it is — vision, architecture, and design decisions.

1 - Vision

Where Itara is going, not just where it is today.

This document describes where Itara is going, not just where it is. It is a living document.

Itara — the open-source specification and its reference implementations — is the cornerstone of this vision, not the whole of it. Everything else on this site describes what exists today, working, open source, and usable. This page describes the future that today’s Itara is the first step toward.


The north star

A production system should have a declared, validated, and executable topology — how its components communicate and connect — that is easy to change without touching business code, without migration ceremony, and without downtime. And it should be able to do so safely: with the confidence that the new topology is correct before it is applied, that every connection is validated, and that nothing can be misconfigured silently.

A future where developers build software, and the topology evolves itself. Not a world where the topology of distributed systems is programmed — a world where it’s designed:

  • Design — engineers draw topology in a visual editor.
  • Validate — a controller validates every change and orchestrates deployment.
  • Observe — the running system is continuously observed against the declared topology.
  • Evolve — the platform safely recommends or performs topology optimisations. Developers focus on business logic. The platform manages topology.

This is physically possible. What has been missing is the architectural model that makes it real.


The core reframe

Today, the topology of a distributed system — which components exist, how they communicate, where they are deployed — is encoded in the system itself. It lives in HTTP clients, message producer configurations, service discovery calls, timeout settings, and retry policies scattered across every service. Changing the topology means changing code. Splitting a service means rewriting clients. Moving from HTTP to a message queue means touching both sides. Architecture decisions made at the start of a project calcify into the codebase.

Worse, topology is invisible. Nobody has the full picture. Changes are made by reading code, making assumptions, and hoping the assumptions hold. The system cannot tell you what it is. It cannot tell you what will break if you change it. It cannot tell you if the change you just made is correct.

Itara reframes this. Topology is not a property of the code — it is a continuously adjustable variable declared in configuration and applied by the wiring agent. Component logic expresses what a system does. The wiring config expresses how the parts connect. These are different concerns and they should live in different places.


What Itara is not

Itara is a thin slice. It concentrates one specific concern — topology — into an explicit and executable layer. The goal is not to own the stack or replace what already works.

Container orchestrators own deployment, scaling, and scheduling. Service meshes own network-level traffic policy between deployment units. Observability platforms own how telemetry is stored, queried, and visualised. Deployment tooling owns how software reaches production. These are all solved problems with mature, well-understood tools. Itara does not compete with them — it declares the topology that sits above and between them, and eventually generates inputs for them from that declaration.

What has no mature solution is the layer between architecture diagrams and running systems — the layer where topology decisions are made, encoded throughout the codebase as side effects, and then lost. That is the layer Itara owns.


The two pillars

Itara is built on two co-equal parts. Neither is sufficient without the other.

The wiring agent makes topology a separate layer. It 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. The agent is language-specific — each supported language has its own implementation — but the wiring config and the component model are language-neutral.

The tooling ecosystem makes that layer safe and manageable. It validates configurations before deployment, catches mismatches and incompatibilities at authoring time, visualises the topology as a graph, closes the loop between declared and actual topology, and guides engineers through changes. Incorrect topologies cannot be deployed silently. The layer Itara introduces is the layer the tooling understands deeply.

The wiring agent moves complexity out of the code. The tooling takes responsibility for that complexity. Together they make topology a first-class engineering artifact — declared explicitly, validated before deployment, and easy to change precisely because the system understands itself completely.


Pillar one: the wiring agent

Zero overhead on colocated calls

When two components share the same process and type system — for example, two JVM components wired as direct, or two Rust components loaded as separate dynamic libraries in the same process — the agent resolves a proxy at startup. At call time, that proxy calls the implementation directly. There is no serialization, no network hop. Nothing happens that isn’t strictly necessary for the call itself — this is a design commitment, not an aspiration.

For components colocated on the same host but running in separate runtimes — such as components written in different languages — the developer declares the local IPC mechanism in the wiring configuration. The runtime uses exactly the mechanism declared and nothing else. No network leaves the host. No transport decision is made autonomously by the agent.

Startup resolution, not call-time decisions

The agent’s job is finished before the first request arrives. All proxies are resolved, all connections are established, all SPIs are loaded. There is no application server sitting between components. There is no sidecar intercepting calls. The wiring happens once. The application runs free.

This is what makes topology change safe to reason about: the state of the system is fully determined by the wiring config at startup time. There are no runtime surprises, no lazy connection establishment, no decisions deferred to call time.

Language-specific, specification-neutral

The agent is implemented separately for each supported language, following the idioms and capabilities of that language. The separation of topology from code does not vary. See Language agnosticism for the full picture.


Pillar two: the tooling ecosystem

The topology compiler

The primary authoring and validation tool is a CLI that understands the system deeply — which components exist, what contracts they implement, what serializers and transports they support, what versions are compatible. It validates every connection in the wiring config before deployment: API contracts, serializer compatibility, version ranges, dependency completeness.

This is the compiler for distributed system topology. It takes intent, validates it against what the system actually contains, and produces a verified artifact. A topology that does not pass validation does not deploy. Incorrect configurations are caught at authoring time, not discovered in production.

The same validation logic is meant to eventually power a visual editor: engineers draw topology on a graph, the tool validates each connection in real time. The CLI and the UI would be the same engine with different rendering layers — everything checkable in one, checkable in the other.

The controller

Above the compiler sits the controller — the intelligent layer that observes the running system, builds a model of its behaviour, and recommends or executes topology changes.

The trust ladder governs how much autonomy the controller is granted:

  1. Self-service — engineer decides, tooling makes it cheap and reversible
  2. Recommendations — controller suggests with reasoning, engineer approves
  3. Prepared actions — one button, fully described, reversible
  4. Full automation — opt-in, scoped, with kill switches and full audit trail

The controller never operates opaquely. Every recommendation is explained. Every automated action is logged and reversible. Trust is built on transparency, not on promises.

The long-range vision

The tooling direction extends further than configuration authoring. As the system matures, the tooling becomes capable of reasoning about the entire lifecycle of a distributed system: validating that a topology change is safe before it is applied, predicting the effect of topology changes on observed behaviour before they are applied, and eventually enforcing that the actual topology matches the declared topology — the same control loop that infrastructure-as-code brought to servers — and driving automated topology decisions that the engineer approves rather than initiates.

The long-range vision: a distributed system that can recompile its own components with updated contracts, redeploy them with a new topology, and verify the result — all driven by the same tooling that started as an interactive CLI. Not autonomous in the sense of opaque, but autonomous in the sense of tireless, consistent, and always verifiable.


Built-in observability

Observability is not an afterthought in Itara — it is a structural property of the architecture. The agent intercepts every call between components at startup. That interception point is the natural place to collect latency, throughput, error rates, and transport overhead without any instrumentation burden on the developer.

Every connection in the topology graph has events attached to it automatically. Engineers can see not just the structure of their system but how every connection behaves — latency, errors, and transport overhead, without writing a single line of monitoring code.

This observability is also what makes the controller trustworthy. Before automation is ever enabled, the engineer can watch the controller’s reasoning against real data and verify that it is correct. A system that cannot be observed cannot be safely automated. Itara treats these as inseparable requirements.


The wiring config as a graph

The wiring config is a directed graph. Nodes are components. Edges are typed connections. The same component can be reached by different callers via different connection types. This is the data structure the tooling will reason about, the visualisation layer will render, and the engineer will eventually plan graphically.

Today the wiring config is a file — a practical entry point for a system at its current scale. As systems grow to hundreds of components and thousands of connections, the backing store will evolve: a document store, a graph database, a relational schema, or something not yet decided. The agent always works from a concrete configuration snapshot, but where that snapshot lives and how it is managed is an operational concern.


Language agnosticism

Itara is not a Java project. Java is the first reference implementation. Rust is the second. More languages follow.

The specification defines the component model, wiring model, transport interface, observability model, and context propagation contract. Any language with sufficient metaprogramming or build-time automation capability — whether through macro systems, bytecode manipulation, annotation processors, or dynamic class loading — can implement the specification. The interface definitions will eventually be replaced by a language-neutral descriptor — components in any language participating in the same topology graph, producing the same distributed traces.

The Rust implementation proved language agnosticism is not aspirational. The same wiring config switches a Java gateway from calling a Java calculator to calling a Rust calculator. The distributed trace shows both spans. The code changes nothing.


The mathematical foundation

Individual components can be modeled as queuing systems. Composition follows Network Calculus — the same cumulative formulation, extended to arbitrary topologies with worst-case delay bounds. The goal: predict the effect of a topology change before making it, the way an engineer analyzes a circuit before building it.

This mathematical work is a research direction, not a committed implementation plan. If it lands, it becomes the foundation for the controller’s predictive reasoning — the difference between a tool that validates topology and a tool that optimises it. Academic collaboration is the realistic path for taking it from theory to a runtime the controller can use.


Why now

The microservices explosion has made the operational pain acute and universal. Kubernetes and Terraform have trained engineers to think in external controllers and declarative topology. Rust makes native agent libraries practical without GC overhead. Academic work on queuing models and feedback control of computing systems has existed for decades without a practical application to pull it into production use.

Itara is the missing application.

2 - Architecture

How the pieces fit together — layers, startup sequence, request flow, and the rules that don’t bend.

This page describes how Itara actually works today — the module structure, the layering rules, what happens at startup, and what happens on a call. For where this is going, see Vision. For the formal, normative behavior every implementation must satisfy, see the wiring configuration and metadata format references.


The core idea, in one paragraph

The Itara agent runs before application code. It reads a wiring config, loads the plugins that config actually requires, generates proxies and listeners for each declared connection, and registers everything in the component registry — before the application handles its first request. By the time your entry point executes, the topology is fully wired. Component code calls other components through plain interface references and never knows whether the call goes in-process or across a network — or which language the other component is written in. The specification itself is language-neutral: a Java component and a Rust component can participate in the same topology, wired by the same config, producing a single distributed trace.


Layers

Itara has four layers. The dependency rule is strict: lower layers know nothing about upper layers. All arrows point downward.

  • Your application — calls components via interfaces, depends on core only
  • Plugins — SPI implementations, pulled in based on whatever the wiring config actually requires
  • Agent — reads config, loads plugins, wires everything before application runs
  • Core — the structure and interfaces (SPIs) everything else builds on, plus the registry and context
graph TD
    App["Your application<br/><i>depends on core only</i>"]
    Plugins["Plugins<br/><i>SPI implementations — whatever<br/>the wiring config requires</i>"]
    Agent["Agent<br/><i>Java: JVM premain</i><br/><i>Rust: itara_init() library</i><br/><br/>reads wiring config · loads plugins<br/>wires everything before application runs"]
    Core["Core<br/><br/>structure · interfaces (SPIs) · registry · context"]
 
    App --> Core
    App --> Agent
    Plugins --> Agent
    Agent --> Core
    Plugins --> Core

Key concepts

Contract — the interface a component exposes to its callers. Says what the component does, not how. Declares operations — inputs, outputs, errors — with no reference to transport, serialization, or deployment. Callers depend only on the contract; the implementation behind it, and the topology, are both invisible to them.

Component — a unit of business logic: a contract, a concrete implementation satisfying it, and an activator that wires it at startup, together. Component code has no knowledge of transport, topology, or deployment — it doesn’t know whether it’s being called locally or remotely, by one caller or many. Components are the fixed units of the system; they don’t split or merge, only their placement in a topology changes.

Activator — instantiates the component. The single composition root per component. Component code itself never touches the registry directly — only the activator does, and only at startup.

Node — a deployment identity. Declared in the wiring config with an id and a component. One component can run as multiple nodes with different topologies. How many instances of a node are running is the orchestrator’s concern — not Itara’s.

Master wiring config — a single YAML file describing the complete topology. Each process reads the same file and self-selects its relevant slice based on which nodes it is responsible for. One source of truth. See the wiring configuration reference for the full format.


Startup sequence

Before application code runs:

sequenceDiagram
    participant App as Application
    participant Agent
    participant Config as Wiring Config
    participant Registry
 
    Agent->>Config: load
    Config-->>Agent: nodes + connections for this process
    Note over Agent: load metadata for available artifacts
    Note over Agent: load required contracts (synchronous and event)
    Note over Agent: load required plugins
    Agent->>Registry: register activators (local components)
    loop For each connection
        alt local node → remote node
            Agent->>Registry: preregister proxy (wraps transport)
        else remote node → local node
            Agent->>Registry: register dispatcher
        else local node → local node
            Agent->>Registry: register direct proxy, establish component scope
        end
    end
    Note over Agent: start inbound listeners
    Agent->>Registry: freeze
    Agent-->>App: startup complete → your code runs

The invariant: by the time the application logic runs, the topology is fully wired, validated, and every inbound listener is started. Topology errors surface at startup with clear messages — never at call time.


Request flow

Direct call (same process)

In a direct connection, the caller holds a reference to a proxy. The proxy fires observability events and calls the implementation directly — nothing that isn’t strictly necessary for the call itself. No serialization, no network. The proxy is what makes observability structural: there is no other path to the implementation.

sequenceDiagram
    participant Caller
    participant Proxy as Proxy (agent-generated)
    participant Impl as Component Implementation
 
    Caller->>Proxy: method call
    Proxy->>Proxy: CALL_SENT
    Note over Proxy: enforce component scope, authN/authZ if configured
    Proxy->>Proxy: CALL_RECEIVED
    Proxy->>Impl: direct method call (no serialization, no network)
    Note over Impl: execute
    Impl-->>Proxy: result
    Proxy->>Proxy: RETURN_SENT
    Proxy->>Proxy: RETURN_RECEIVED
    Proxy-->>Caller: result

No serialization. No network. The component implementation is unaware of the proxy — it just executes. The four events are structural properties of the platform that cannot be removed, whether the connection is colocated or remote.

Remote call (separate processes)

sequenceDiagram
    participant Caller
    participant Proxy as Proxy (caller process)
    participant Net as Network
    participant Dispatcher as Dispatcher (callee process)
    participant Impl as Component Implementation
 
    Caller->>Proxy: method call
    Proxy->>Proxy: CALL_SENT
    Note over Proxy: serialize args, attach propagated headers
    Proxy->>Net: send
    Net->>Dispatcher: deliver
    Note over Dispatcher: restore context from propagated headers
    Note over Dispatcher: authenticate/authorize if configured
    Note over Dispatcher: deserialize args
    Dispatcher->>Dispatcher: CALL_RECEIVED
    Dispatcher->>Impl: method call
    Note over Impl: execute
    Impl-->>Dispatcher: result
    Dispatcher->>Dispatcher: RETURN_SENT
    Note over Dispatcher: serialize result
    Dispatcher-->>Net: send
    Net-->>Proxy: deliver
    Note over Proxy: deserialize result
    Proxy->>Proxy: RETURN_RECEIVED
    Proxy-->>Caller: result

The specific wire format — HTTP verbs and URLs, a Kafka topic and partition, or anything else — is entirely up to the transport implementation. Nothing about how a call crosses a process boundary is architecturally fixed except the four events firing at the topology boundary on each side, and whatever headers get attached along the way.


Observability event model

Every component interaction — direct or remote, Java or Rust — emits four events, firing precisely at the boundary between the business layer and the topology layer:

EventSideFires
CALL_SENTCaller proxyBusiness layer hands off to topology layer
CALL_RECEIVEDCallee dispatcherTopology layer hands off to business layer
RETURN_SENTCallee dispatcherBusiness layer hands the result back to topology layer
RETURN_RECEIVEDCaller proxyTopology layer hands the result back to business layer

This placement — at the layer boundary, not tied to any particular mechanism like serialization — is what makes topology overhead measurable regardless of transport. Everything between CALL_SENT and CALL_RECEIVED is topology cost, by construction: serialization, network transit, deserialization for a remote call; nothing at all for a direct one. This gives:

  • Caller span (CALL_SENT → RETURN_RECEIVED): full round trip including whatever the topology layer did on both sides
  • Callee span (CALL_RECEIVED → RETURN_SENT): pure component processing time, independent of transport entirely
  • Serialization and network cost: whatever’s left — the gap between the two spans

Plugins can add their own custom spans for their own diagnostic detail. The four structural events are different: only the proxy and dispatcher ever emit them. A plugin can add detail alongside them, never in place of them.


Boundary enforcement

Components never call each other directly — not even when colocated in the same process. Each has its own scope. The only path between two components is a proxy the agent prepared from the wiring config, and that holds identically whether the connection is direct or remote. Colocating two components for performance is a placement decision; it never implies they can reach each other without something declared between them.


The SPI pattern

All plugin kinds follow the same discovery pattern, in every language — the specific kinds extend over time, the mechanism does not:

  • Each artifact ships a .itara metadata file declaring its kind, id, version, and capabilities.
  • The agent reads all metadata files before loading any artifact — validation happens before execution.
  • The agent loads only what the wiring config requires.
  • Loaded implementations register with their respective SPI registry in core.

A component author never needs to know which plugins, if any, a given connection uses; that knowledge lives entirely in the wiring config.


Hard constraints

These are not guidelines — they are architectural invariants:

  • Core has no external dependencies. If a proposed change requires adding a dependency to core, the design is wrong.
  • Application code depends on core only. It never depends on the agent or any plugin directly.
  • Plugins never import each other. They are connected by the agent, not by depending on one another.
  • No topology decisions at call time. Proxies, listeners, and dispatchers are resolved at startup. Nothing is looked up during request handling.
  • Observability is fired by the proxy/dispatcher layer, not by plugins. A plugin can add detail; it cannot replace the structural events or decide not to fire them.
  • Colocation is a placement decision, not a trust decision. Nothing about how two components reach each other changes based on whether they share a process.
  • Direct calls add nothing beyond what’s strictly necessary for the call itself. No serialization, no network — that’s the whole guarantee; it isn’t a claim that literally nothing else can ever run on that path.