1 - Get Started

Install Itara, write one component, wire two together, run it, see a trace.

2 - Guides

How to accomplish specific tasks with Itara — colocating services, using message formats, writing plugins, and more.

3 - Reference

Wiring configuration, .itara metadata, the CLI, the glossary, and the specification.

3.1 - Glossary

The vocabulary of the Itara project — shared terms for contributors, adopters, and anyone explaining Itara to others.

Clear, shared vocabulary matters: if people can talk about the project fluently, the ideas travel. If the words are missing or inconsistent, the point gets lost. This is the reference for that vocabulary.

Agent

See Wiring Agent.

Authentication

Verifying that a caller’s claimed identity (see Identity) is genuine, at the topology layer.

Authentication is connection-level and pluggable: an implementation receives whatever identity signal is available for a connection and either produces a verified identity or rejects the call outright. Used without qualification, this term refers to authenticating a calling node, not an end user or account.

Authorization

Deciding whether an authenticated caller is permitted to invoke a specific operation — a topology-layer concern about which node may call what.

Authorization is connection-level and pluggable: an implementation reads the identity authentication produced, together with the operation being invoked, and decides allow or deny. Used without qualification, this term refers to node-to-node permission, not end-user permission.

Colocation

Placing two or more components in the same process so that calls between them are direct, in-memory, and zero-overhead.

When components are colocated, the agent resolves a direct proxy at startup. At call time, the proxy fires the four observability events and calls the implementation directly — no serialization, no network hop, no transport overhead of any kind. The component code is identical regardless of whether the call is colocated or distributed.

Colocation is declared in the wiring configuration by using the direct transport type on a connection. It is a topology decision, not a code decision.

Component

A unit of business logic with a defined contract and a concrete implementation. It knows nothing about how it is called.

A component consists of three things: a contract (an interface declaring what it does), an implementation (a class or struct satisfying that contract), and an activator (a factory method that wires its dependencies at startup). The component code itself has no knowledge of transport, topology, or deployment. It does not know whether it is being called locally or remotely, by one caller or many, over HTTP or Kafka or a direct in-process call. That knowledge lives in the wiring configuration.

Components are the fixed units of the system. They do not split or merge — only their placement in a topology changes.

Component Scope

Defines the boundary of what a running component instance can reach, and carries what’s needed to enforce it — established and maintained exclusively by the agent.

A component’s scope determines which other components’ proxies are actually available to it, prepared by the agent from the wiring configuration. It also carries whatever the agent needs to enforce that boundary — at minimum, which node in the topology graph is currently in control. This is not identity in the authentication sense; it’s simply the agent’s own record of where in the graph execution currently is.

Colocation does not imply shared scope. Two components sharing a process for a zero-overhead direct connection (see Colocation) remain distinct — each has its own scope, and neither can assume the other’s.

Connection

A declared relationship between two nodes, specifying that one calls the other and how.

A connection is directed: it has a caller node and a callee node. It declares the transport type (direct, HTTP, Kafka, or any other supported transport) and any additional configuration the transport requires. All failure semantics — retry behaviour, timeouts, circuit breaking — are connection-level configuration, not component-level code.

Contract

The interface a component exposes to its callers. It says what the component does, not how.

A contract declares operations — inputs, outputs, and errors. It contains no reference to transport mechanism, serialization format, or deployment infrastructure. Callers depend only on the contract. The implementation behind it is invisible to them, and so is the topology.

Deployment Group

The set of nodes that run in the same process because they are connected by direct calls.

Deployment groups are derived automatically by the wiring agent and the CLI from the connection graph. A deployment group is not declared — it is a consequence of topology decisions. Nodes connected by direct calls must be colocated; the deployment group is the set of all nodes that must be together as a result.

A single component may be referenced by multiple nodes — in different deployment groups, with different topology roles. The node is the deployment identity; the component is the business logic behind it. Deployment groups are about nodes, not components.

itara inspect shows the derived deployment groups for any wiring configuration. They are the practical answer to the question: what goes together, and what runs separately?

Distribution

Placing components in separate processes so that calls between them travel over a transport.

When components are distributed, calls cross a process boundary. The transport — HTTP, Kafka, or any other supported mechanism — carries the call. The component code does not change. The transport cost becomes visible in the traces as the gap between the caller’s and callee’s observability spans.

Distribution is the default assumption of traditional microservice architectures. Itara makes it an explicit, reversible, config-level decision rather than an architectural commitment baked into the code.

Failure Semantics

How a connection behaves when things go wrong — declared in configuration, invisible to component code.

Failure semantics are connection-level configuration. A single pluggable implementation owns the complete failure strategy for a connection — how it handles timeouts, retries, circuit breaking, or any other failure-related concern. The component code is unaware of any of it; it simply makes calls. Idempotency is declared in the API artifact metadata and read by the proxy at startup, so methods that cannot be safely repeated are protected without any explicit handling in the business logic.

Identity

The claimed or verified origin of a call — a property of the node, not the deployment unit it happens to run in.

Colocated nodes do not automatically share an identity just because they share a process (see Colocation); different identity rules can still apply to each.

Message Format

A structural encoding scheme whose code generation tooling produces a contract’s types, declared once for the whole contract.

Some contracts define their parameter and return types by hand, in the implementation language, as ordinary classes or structs. Others declare a message format — such as Protocol Buffers — and generate those types from a schema instead. Either way, the contract is still just an interface: it says what a component does, not how it is transmitted. A message format constrains the shape of the data; it says nothing about serialization, transport, or topology.

A contract commits to a message format entirely or not at all — an API artifact does not mix hand-written and generated types across its methods. Given an API artifact, one fact answers whether its types are plain or generated, for every method it declares.

Message format is independent of serializer choice. A serializer converts a contract’s types to and from bytes; a message format determines what those types are. The two vary independently — which serializers can handle a given message format is a separate, per-serializer question.

Node

A named deployment position in the topology. It says where a component runs.

A node has an identifier and references a component. Multiple nodes may reference the same component — they are independent positions in the topology graph, not copies of each other. How many instances of a node run at any time is the orchestrator’s concern, not Itara’s.

Observability Events

Four events that fire precisely at the boundary between the business layer and the topology layer, on every call, regardless of transport.

Every call between components — direct or remote, synchronous or asynchronous — emits four events: CALL_SENT and RETURN_RECEIVED on the caller side, CALL_RECEIVED and RETURN_SENT on the callee side.

The placement is deliberate. CALL_SENT fires when the business layer hands a call to the topology layer. CALL_RECEIVED fires when the topology layer hands it to the callee’s business layer. RETURN_SENT and RETURN_RECEIVED are the mirror on the way back. Everything that happens between those events — serialization, network transit, deserialization — is topology overhead, directly measurable as the gap between the caller span and the callee span.

These events are structural and non-optional: they are fired by the proxy and dispatcher at the layer boundary, not by the transport. A transport that skips them cannot exist — it never touches the event model.

Placement

The topology decision of where a component runs and how it connects to others.

Placement is what changes when you move a component from one deployment group to another, or change its transport. The component itself does not change — only its position in the topology and the communication path to and from it. Placement decisions are made in the wiring configuration.

Proxy

The object a caller holds instead of the real implementation. It is transparent to the caller.

A proxy satisfies a component contract. The caller cannot tell the difference between a proxy and the real implementation — the interface is identical. The proxy is what makes topology transparent to component code: it handles serialization, transport, context propagation, and observability events without the caller knowing any of it is happening.

For direct connections, the proxy fires the four observability events and calls the implementation directly. No serialization, no network. The proxy is still present — it is what makes observability structural and non-optional.

The Three Layers

Business logic, contracts, and topology are three distinct concerns. Itara keeps them separate.

Every Itara system has three layers:

  • Business layer — the implementation: what the system actually does. Written by developers. Has no knowledge of transport, topology, or infrastructure.
  • Contract layer — the interfaces: what each component exposes to its callers. Defines operations, inputs, outputs, and errors. Contains no transport or topology knowledge.
  • Topology layer — the wiring: which nodes exist, how they connect, what transport they use, how failures are handled. Lives entirely in the wiring configuration and the proxies and listeners the wiring agent constructs from it. The separation is the point. Business logic does not leak into topology. Topology does not leak into business logic. Changing one does not require touching the other.

Topology

The structure of a system: which components exist, how they connect, and where they run.

Topology is the concern Itara makes explicit. In a traditional distributed system, topology is implicit — encoded across HTTP clients, retry policies, service discovery calls, and timeout configurations scattered through the codebase. With Itara, topology is declared in the wiring configuration, the single source of truth for the entire system structure.

Topology is not a deployment detail. It is an architectural artifact — declared, validated before deployment, and changeable without touching business code.

Transport

A pluggable mechanism for carrying calls between distributed components.

A transport provides two things: an outbound proxy for the caller side, and an inbound listener for the callee side. The component code never sees the transport. Transports are declared in the wiring configuration and loaded by the agent at startup. Transports can be provided as plugin artifacts.

Wiring Agent

The part of Itara that reads the wiring configuration, wires all components together before the application starts, and then steps aside.

The wiring agent runs before application code. It reads the wiring configuration, loads transport and serializer plugins, constructs proxies and listeners for every declared connection, and registers everything in the component registry. By the time the first application thread runs, the topology is fully wired and all decisions are made. The wiring agent is not present in the call path at runtime — it does not intercept calls, route dynamically, or make decisions on the fly. The proxies it created do the work; the wiring agent is done.

In Java, the wiring agent is a JVM premain agent. In Rust, it is an itara_init() library call. The mechanism is language-specific; the behaviour is defined by the specification.

Wiring Configuration

The single file that describes the complete topology of a system.

The wiring configuration declares which nodes exist, which components they run, and how they connect. It is the authoritative source of topology information. The agent reads it at startup. The CLI validates it before deployment. Nothing about how components communicate exists anywhere else.

Changing the wiring configuration changes the topology. No code changes.

3.2 - API Documentation

Generated class and method reference for the Java and Rust implementations.

Full class and method documentation is generated directly from source and hosted by each language ecosystem’s own standard tooling — not duplicated here, so it never drifts out of sync with what’s actually published.

Java

itara-core and itara-agent on javadoc.io

Rust

Rust crates are not yet published to crates.io. Once they are, generated documentation will be available automatically at docs.rs under the published crate name.

3.3 - Tools

Reference documentation for itara-cli and any future Itara tooling.

3.3.1 - itara-cli

The command-line tool for inspecting and validating an Itara wiring configuration.

itara is the command-line tool for working with an Itara wiring configuration — inspecting a topology at a glance, and validating it for logical mistakes before it’s deployed. It’s implemented once, in Rust, and operates purely on the file formats — the wiring config YAML and .itara metadata TOML files — so it works identically regardless of which language (Java or Rust) actually produced the artifacts being wired together; there’s simply nothing language-specific for it to know about.

itara <COMMAND> [OPTIONS]

Two subcommands are available:

CommandPurpose
itara inspectPrint a human-readable summary of the topology described by a wiring config.
itara verifyValidate the logical correctness of a wiring config, optionally cross-checked against a directory of .itara metadata files.

Both commands take the path to a wiring config file as their first positional argument, parse it with the same loader described in the wiring configuration doc (including environment variable substitution and YAML merge-key resolution), and exit non-zero if the file can’t be read or parsed at all — before any command-specific logic runs.

Installation / building

There’s no published binary yet — build it from the itara-cli crate in the source repository:

cargo build --release -p itara-cli
# binary at target/release/itara

or install it onto your PATH:

cargo install --path itara-cli

itara inspect

itara inspect [OPTIONS] <CONFIG>

Prints a readable summary of a wiring config’s topology: every node, every internal connection, the deployment groups the topology implies, and an ASCII arrow graph of how calls flow through it. Intended for humans skimming a config, not for machine consumption — there’s no --format json or similar; it’s plain text.

Arguments

ArgumentRequiredDescription
<CONFIG>YesPath to the wiring config file.

Options

OptionDescription
--no-graphSuppress the Graph section.
--no-groupsSuppress the Deployment groups (derived) section.
--no-eventsSuppress the Emits: / Listens to: lines within deployment groups. Only meaningful when groups are shown at all.
--node <id>Show only the given node and its direct connections, instead of the whole topology. Repeatable — pass it multiple times to focus on a small neighbourhood of several nodes at once. An id that doesn’t exist in the config produces a warning on stderr but isn’t fatal.

What each section shows

Nodes — every node in the (possibly --node-filtered) config, one per line, with its kind-specific detail (component: <id> or virtual: <contract> @ <address>). A node with at least one external inbound connection (a connection with no caller block) is marked (external entry point).

Connections — every internal connection (i.e. connections with a non-empty caller block — external inbound connections are omitted from this list since they have no interesting from → to shape to show), as <id>: <from> → <to> [<transport-id>].

Deployment groups (derived) — component nodes joined by one or more direct (in-process/colocated) connections are grouped together, since they’ll end up running in the same process. This is a connected-components search over the subgraph of direct edges only — every other transport type is treated as crossing a deployment boundary. Groups are labeled A, B, C, …, Z, AA, AB, … in the order their first node is encountered (which follows node declaration order in the file). Virtual nodes never form or join a group — grouping only concerns deployable components. For each node in each group, its non-direct inbound/outbound connections are also listed:

  • Receives: external <transport> on <port> — an external inbound connection.
  • Receives: <from> via <transport> on :<port> — an inbound connection from another node.
  • Calls: <to> via <transport> — an outbound connection to another (non-virtual) node.
  • Emits: <to> (<contract>) — an outbound connection to a virtual node (suppressed by --no-events).
  • Listens to: <from> (<contract>) — an inbound connection from a virtual node (suppressed by --no-events).

This is the practical answer to “what goes together, what runs separately, what needs to start before what” — the basis for generating Docker Compose services, Kubernetes manifests, or startup ordering, derivable from the config alone rather than reconstructed by reading code. The full example below, from Itara’s own demo, shows a realistic case: three groups, one of them multi-node, with both synchronous and event-driven connections mixed together.

Graph — an attempt to render the topology as arrow chains, e.g.:

[external] --http:8080--> [gatewayNode] --http:8081--> [calculatorNode]

Each external entry point starts its own chain, which is walked forward through outbound connections as far as it can go in a straight line. The walk stops the moment it reaches a branch point (a node with more than one outbound connection) or a merge point (a node with more than one inbound connection) — at that point the chain is left as-is, and any connections not swept up into a compressed chain are rendered as their own individual line instead, annotated with (id: <connection-id>) so an otherwise-ambiguous branch can still be traced back to a specific entry in the config. The section is omitted entirely when the config has no connections at all.

Exit codes

CodeMeaning
0Success.
1The config file could not be read or parsed, or (when --node is combined with a config that fails re-validation after filtering) filtering failed.

--node referencing an id that doesn’t exist in the config is a warning on stderr, not a failure — inspection still proceeds with whatever’s left.

Full example

From Itara’s own demo — an order-processing system with five components and three event channels, mixing direct, http, and kafka connections in one config:

nodes:
  - id: "fulfilmentNode"
    component: "fulfilment"
  - id: "inventoryNode"
    component: "inventory"
  - id: "notificationNode"
    component: "notification"
  - id: "paymentNode"
    component: "payment"
  - id: "orderNode"
    component: "order"
  - id: "orderReservedChannel"
    kind: virtual
    contract: "order-events/order-reserved"
    address: "demo.events.order-reserved"
  - id: "orderFulfilledChannel"
    kind: virtual
    contract: "fulfilment-events/order-fulfilled"
    address: "demo.events.order-fulfilled"
  - id: "orderCancelledChannel"
    kind: virtual
    contract: "fulfilment-events/order-cancelled"
    address: "demo.events.order-cancelled"
 
connections:
  - id: "external-to-order"
    callee:
      nodeId: "orderNode"
    transport:
      id: http
      params:
        host: "localhost"
        port: 8081
    serializer:
      id: "json"
  - id: "external-to-inventory"
    callee:
      nodeId: "inventoryNode"
    transport:
      id: http
      params:
        host: "localhost"
        port: 8081
    serializer:
      id: "json"
  - id: "order-to-inventory"
    caller:
      nodeId: "orderNode"
    callee:
      nodeId: "inventoryNode"
    transport:
      id: direct
  - id: "order-to-fulfilment"
    caller:
      nodeId: "orderNode"
    callee:
      nodeId: "fulfilmentNode"
    transport:
      id: direct
  - id: "order-to-payment"
    caller:
      nodeId: "orderNode"
    callee:
      nodeId: "paymentNode"
    transport:
      id: http
      handleTimeout: true
      params:
        host: payment
        port: 8083
    serializer:
      id: "json"
    failureSemantics:
      id: built-in
      maxRetry: 3
      timeout: 5s
      params:
        waitDuration: 500ms
        retryRuntime: "true"
  - id: "order-to-orderReserved"
    caller:
      nodeId: "orderNode"
    callee:
      nodeId: orderReservedChannel
    transport:
      id: kafka
      params:
        bootstrapServers: "kafka:29092"
    serializer:
      id: "json"
  - id: "order-to-orderFulfilled"
    caller:
      nodeId: "orderNode"
    callee:
      nodeId: orderFulfilledChannel
    transport:
      id: kafka
      params:
        bootstrapServers: "kafka:29092"
    serializer:
      id: "json"
  - id: "order-to-orderCancelled"
    caller:
      nodeId: "orderNode"
    callee:
      nodeId: orderCancelledChannel
    transport:
      id: kafka
      params:
        bootstrapServers: "kafka:29092"
    serializer:
      id: "json"
  - id: "orderReserved-to-notification"
    caller:
      nodeId: "orderReservedChannel"
    callee:
      nodeId: "notificationNode"
    transport:
      id: kafka
      params:
        bootstrapServers: "kafka:29092"
        consumerGroup: "order-consumer-group"
    serializer:
      id: "json"
  - id: "orderFulfilled-to-notification"
    caller:
      nodeId: "orderFulfilledChannel"
    callee:
      nodeId: "notificationNode"
    transport:
      id: kafka
      params:
        bootstrapServers: "kafka:29092"
        consumerGroup: "order-consumer-group"
    serializer:
      id: "json"
  - id: "orderCancelled-to-notification"
    caller:
      nodeId: "orderCancelledChannel"
    callee:
      nodeIdto: "notificationNode"
    transport:
      id: kafka
      params:
        bootstrapServers: "kafka:29092"
        consumerGroup: "order-consumer-group"
    serializer:
      id: "json"
$ itara inspect demo/wiring-monolith.yaml
Itara topology — demo/wiring-monolith.yaml
 
Nodes:
  fulfilmentNode        component: fulfilment
  inventoryNode         component: inventory                     (external entry point)
  notificationNode      component: notification
  paymentNode           component: payment
  orderNode             component: order                         (external entry point)
  orderReservedChannel  virtual:   order-events/order-reserved @ demo.events.order-reserved
  orderFulfilledChannel virtual:   fulfilment-events/order-fulfilled @ demo.events.order-fulfilled
  orderCancelledChannel virtual:   fulfilment-events/order-cancelled @ demo.events.order-cancelled
 
Connections:
  order-to-inventory:             orderNode → inventoryNode [direct]
  order-to-fulfilment:            orderNode → fulfilmentNode [direct]
  order-to-payment:               orderNode → paymentNode [http]
  order-to-orderReserved:         orderNode → orderReservedChannel [kafka]
  order-to-orderFulfilled:        orderNode → orderFulfilledChannel [kafka]
  order-to-orderCancelled:        orderNode → orderCancelledChannel [kafka]
  orderReserved-to-notification:  orderReservedChannel → notificationNode [kafka]
  orderFulfilled-to-notification: orderFulfilledChannel → notificationNode [kafka]
  orderCancelled-to-notification: orderCancelledChannel → notificationNode [kafka]
 
Deployment groups (derived):
  Group A: fulfilmentNode, orderNode, inventoryNode
    fulfilmentNode (fulfilment)
    orderNode (order)
      Receives: external http on :8081
      Calls:    inventoryNode via direct
      Calls:    fulfilmentNode via direct
      Calls:    paymentNode via http
      Emits:       orderReservedChannel (order-events/order-reserved)
      Emits:       orderFulfilledChannel (fulfilment-events/order-fulfilled)
      Emits:       orderCancelledChannel (fulfilment-events/order-cancelled)
    inventoryNode (inventory)
      Receives: external http on :8081
 
  Group B: notificationNode
    notificationNode (notification)
      Listens to:  orderReservedChannel (order-events/order-reserved)
      Listens to:  orderFulfilledChannel (fulfilment-events/order-fulfilled)
      Listens to:  orderCancelledChannel (fulfilment-events/order-cancelled)
 
  Group C: paymentNode
    paymentNode (payment)
      Receives: orderNode via http on :8083
 
Graph:
  [external] --http:8081--> [orderNode]
  [external] --http:8081--> [inventoryNode]
  [orderNode] --direct--> [inventoryNode]  (id: order-to-inventory)
  [orderNode] --direct--> [fulfilmentNode]  (id: order-to-fulfilment)
  [orderNode] --http:8083--> [paymentNode]  (id: order-to-payment)
  [orderNode] --kafka--> [orderReservedChannel]  (id: order-to-orderReserved)
  [orderNode] --kafka--> [orderFulfilledChannel]  (id: order-to-orderFulfilled)
  [orderNode] --kafka--> [orderCancelledChannel]  (id: order-to-orderCancelled)
  [orderReservedChannel] --kafka--> [notificationNode]  (id: orderReserved-to-notification)
  [orderFulfilledChannel] --kafka--> [notificationNode]  (id: orderFulfilled-to-notification)
  [orderCancelledChannel] --kafka--> [notificationNode]  (id: orderCancelled-to-notification)

fulfilmentNode and inventoryNode both join orderNode’s group despite inventoryNode also having its own external entry point — group membership follows the direct-connection subgraph only, independent of what else a node is reachable from. paymentNode and notificationNode end up in their own single-node groups because every connection reaching them crosses a transport boundary — http for one, kafka for the other — neither of which is direct.

itara verify

itara verify [OPTIONS] <CONFIG>

Runs a battery of logical checks against a wiring config and reports every problem found, each as an ERROR or a WARNING. Errors indicate the config is broken; warnings flag something suspicious that’s still valid. Meant to be run in CI or pre-deploy — the exit code tells you pass/fail, the output tells you why. Introducing a topology layer creates an obligation: a wiring config that can be misconfigured silently is not a step forward.

Arguments

ArgumentRequiredDescription
<CONFIG>YesPath to the wiring config file.

Options

OptionDescription
--metadata-dir <path>Path to a directory of .itara metadata files (see the metadata format reference). Enables the checks that need to cross-reference metadata — known transport/authentication/authorization types, API version compatibility, timeout capability, transport interrupt safety, and serializer compatibility. Without it, those checks are skipped and a warning is emitted saying so.
--skip <check>Skip a specific check by name. Repeatable. Mutually exclusive with --only.
--only <check>Run only the specified check(s). Repeatable. Mutually exclusive with --skip.

An unrecognized name passed to either --skip or --only is a hard error (exit 1) before the config file is even read, listing every valid check name. Passing both --skip and --only in the same invocation is rejected by argument parsing itself.

Checks

Check nameSeverityRequires --metadata-dirWhat it flags
duplicate-idsErrorNoThe same node id declared more than once.
connection-id-uniquenessErrorNoThe same connection id declared more than once.
self-connectionsErrorNoA connection whose caller.nodeId and callee.nodeId are the same node.
direct-external-conflictErrorNoA connection using the direct transport with no caller block — a direct call is in-process and can’t have an external caller.
outbound-ambiguityErrorNoA node with outbound connections to two or more different node ids that all resolve to the same component — the agent would have no way to pick one at dispatch time.
orphaned-nodesErrorNoA node declared in nodes that no connection (as either caller or callee) ever references.
orphaned-connectionsErrorNoA connection whose caller or callee references a node id that isn’t declared in nodes.
virtual-no-producersWarningNoA virtual node with no inbound connection — nothing will ever publish to it.
virtual-no-consumersWarningNoA virtual node with no outbound connection — nothing will ever consume from it.
virtual-transport-mismatchWarningNoA virtual node whose inbound/outbound connections don’t all agree on the same transport type.
unknown-transportErrorYesA connection’s transport.id (other than the built-in direct) has no matching kind = "transport" metadata file.
unknown-authenticationErrorYesA connection’s authentication.id (other than the built-in noop) has no matching kind = "authentication" metadata file.
unknown-authorizationErrorYesA connection’s authorization.id (other than the built-in noop) has no matching kind = "authorization" metadata file.
api-version-compatibilityErrorYesFor a connection between two component nodes: the caller’s declared [api-dependencies] version for the callee doesn’t satisfy the callee’s declared api-version requirement (checked with semver). Also errors (rather than silently skipping) if either side’s metadata, or the specific dependency declaration, can’t be found — see caveats below.
timeout-capabilityError and WarningYesA connection declares a failureSemantics.timeout or absoluteTimeout that its transport and/or failure-semantics implementation isn’t capable of enforcing the way it’s configured — see details below.
transport-interrupt-safetyErrorYesA connection configures external timeout enforcement (failureSemantics.handleTimeout = true) on a transport whose metadata declares externally-interruptible = false.
serializer-compatibilityWarningYesA connection’s serializer isn’t confirmed compatible with the callee API’s declared [contract] message format or [serializers] supported list — see details below.

Without --metadata-dir, only the first ten checks run; the metadata-backed checks are skipped entirely, and a single warning is added to the output: “no –metadata-dir provided — API version, known transport, timeout, and transport interrupt checks are skipped”.

api-version-compatibility caveats

This check only evaluates connections between two component nodes (it skips external connections, and connections touching a virtual node on either side). Where it does apply, every one of the following also produces its own ERROR rather than silently skipping the connection: no metadata found for the caller component; no metadata found for the callee component; the caller declares no [api-dependencies] at all; the caller’s [api-dependencies] doesn’t include an entry for the callee; the caller’s declared dependency version isn’t valid semver; or the callee doesn’t declare an api-version (or declares an invalid semver range for it). In other words: if you’ve opted into this check via --metadata-dir, an incomplete metadata picture is treated as a problem to fix, not something to shrug off.

timeout-capability details

This check only looks at non-external, non-direct connections that declare a failureSemantics block with a timeout and/or absoluteTimeout. Within that scope it can raise several distinct issues on the same connection:

  • Error — transport.handleTimeout = true but the transport’s metadata says native-call-timeout = false (or the transport has no metadata at all).
  • Error — failureSemantics.handleTimeout = true but the failure-semantics implementation’s metadata says supports-external-timeout = false (or has no metadata at all).
  • Error — failureSemantics.handleTimeout = true but the transport’s metadata says externally-interruptible = false.
  • Warning — a timeout is declared but neither transport.handleTimeout nor failureSemantics.handleTimeout is set — the value will be threaded through but nothing will actually enforce it.
  • Warning — both transport.handleTimeout and failureSemantics.handleTimeout are set — both will independently try to enforce the timeout, and which one wins is a race.
  • Error — absoluteTimeout is declared but the failure-semantics implementation doesn’t declare supports-external-timeout = true (or has no metadata at all) — an absolute timeout can only ever be enforced externally, so this is required whenever it’s set, independent of handleTimeout.

serializer-compatibility details

Skips direct connections (nothing is ever serialized on them) and connections whose callee doesn’t resolve to a component. The check itself only applies when the callee API’s metadata declares something to check against — a non-empty [contract].message-format or a non-empty [serializers].supported list. An API declaring neither produces no issue at all for this check (not even a warning): there’s no restriction to be incompatible with. Where the check does apply, a connection’s configured serializer is considered compatible if either:

  • it appears (by id, with a version satisfying the declared semver range) in the API’s [serializers].supported list, or
  • the API’s declared [contract].message-format appears in the serializer’s own [serializer.capabilities].message-formats.

If neither holds — or if the serializer’s own metadata can’t be found at all — the connection is flagged with a WARNING (never an ERROR): this check reports “we can’t confirm this is safe,” not “this is broken,” since the agent itself doesn’t enforce serializer/API compatibility at runtime either.

Exit codes

CodeMeaning
0The config parsed successfully and no ERROR-severity issues were found. Warnings do not affect the exit code.
1The config could not be read/parsed, an unrecognized --skip/--only check name was given, or at least one ERROR-severity issue was found.

Output format

<✓|✗> itara verify — <path>
 
  <N> nodes, <M> connections
 
  <label>  <message>
  <label>  <message>
  ...
 
  <summary line>

✓ is printed when there are no errors (warnings may still be present); ✗ when there is at least one error. If the config fails to parse, only the header, a single ERROR line for the parse failure, and a 1 error summary are printed — none of the individual checks run. If there are no issues at all (not even warnings — i.e. --metadata-dir was supplied and everything passed), the line No issues found. is printed instead of a check list and summary.

Examples

A clean run against the demo’s monolith topology, with a metadata directory supplied:

$ itara verify --metadata-dir demo/metafiles/ demo/wiring-monolith.yaml
✓ itara verify — demo/wiring-monolith.yaml
 
  8 nodes, 11 connections
 
  No issues found.

The same topology with two problems deliberately introduced — the order-to-fulfilment connection removed (orphaning fulfilmentNode, since nothing else references it) and handleTimeout dropped from order-to-payment’s transport block while a timeout is still declared — triggers orphaned-nodes and timeout-capability together, one an error and one a warning:

$ itara verify --metadata-dir demo/metafiles/ demo/wiring-informed-with-error.yaml
✗ itara verify — demo/wiring-informed-with-error.yaml
 
  8 nodes, 10 connections
 
  ERROR  node 'fulfilmentNode' is declared but not referenced in any connection
  WARN   connection 'orderNode' → 'paymentNode': a timeout is declared but neither the transport nor the failure semantics implementation is configured to enforce it — the timeout value will be passed to the transport but nothing will act on it
 
  1 error, 1 warning

Narrowing to a single check, against the same demo config:

$ itara verify --metadata-dir demo/metafiles/ demo/wiring-monolith.yaml --only unknown-transport
✓ itara verify — demo/wiring-monolith.yaml
 
  8 nodes, 11 connections
 
  No issues found.

3.4 - Wiring Configuration

The YAML format that declares topology — nodes, connections, and everything a connection carries.

The wiring configuration is the YAML file that describes the topology of an Itara deployment: which nodes exist, and how they’re connected to one another (or to the outside world).

This document describes the file format itself — the schema, the required and optional fields, and what makes a document valid.

Top-level structure

nodes:
  - id: "gatewayNode"
    component: "gateway"
  - id: "calculatorNode"
    component: "calculator"
 
connections:
  - id: "gateway-to-calculator"
    caller:
      nodeId: "gatewayNode"
    callee:
      nodeId: "calculatorNode"
    transport:
      id: http
      params:
        host: "${CALC_HOST:-localhost}"
        port: "${CALC_PORT:-8081}"
    serializer:
      id: json
FieldTypeRequiredDescription
nodeslist of NodeNo (defaults to empty)Every node in the topology.
connectionslist of ConnectionNo (defaults to empty)Every connection between nodes.

An empty or comment-only file is valid and parses to an empty configuration (no nodes, no connections).

Environment variable substitution

Before the YAML is parsed, the raw file content goes through a textual substitution pass. Any occurrence of:

  • ${VAR_NAME} — replaced with the value of environment variable VAR_NAME.
  • ${VAR_NAME:-default} — replaced with the value of VAR_NAME if set, otherwise the literal default. is substituted in-place, before the YAML parser ever sees the file. This means a substituted value is always plain text at parse time — e.g. an env var substituted into a port: field arrives as a numeric string and the YAML parser coerces it the same way a literal 8081 would be.

A variable with no default that isn’t set in the environment is left as the literal placeholder text (${VAR_NAME}) in the document.

Substitution applies anywhere in the file — it’s a plain text pass over the whole document, not scoped to particular fields.

YAML merge keys and anchors

Standard YAML anchors/aliases and merge keys (<<) are supported and resolved before the document is mapped onto the config model — so shared blocks (e.g. a common transport.params map used by several connections) can be factored out with a YAML anchor and merged in with <<.

Nodes

A node is either a component node or a virtual node. The two share an id and are distinguished by the kind field.

nodes:
  - id: "orderServiceNode"
    component: "order-service"        # component node (kind omitted → defaults to "component")
 
  - id: "inventoryNode"
    kind: component                   # component node (kind explicit)
    component: "inventory"
 
  - id: "orderPlacedChannel"
    kind: virtual
    contract: "order-events/order-placed"
    address: "demo.events.order-placed"

Common fields

FieldTypeRequiredDescription
idstringYesThis node’s own identifier. Must be unique, non-blank and match the id character set. Used elsewhere in the file (connections[].caller.nodeId / connections[].callee.nodeId) to refer to this node.
kindstring: component | virtualNo — defaults to componentDiscriminates which node type this is. Case-insensitive (Component, VIRTUAL, etc. are all accepted and normalized). A config without a kind field is a config full of component nodes.

Component nodes (kind: component)

A component node is a deployable component instance — something with an activator and a contract implementation.

FieldTypeRequiredDescription
componentstringYesThe component id. Must match the artifact.id of a kind = "component" metadata file discoverable by the agent.

Virtual nodes (kind: virtual)

A virtual node represents a communication channel with no component implementation of its own — a named point that decouples producers from consumers via a broker (e.g. a Kafka topic). It has no activator and no agent-managed lifecycle.

FieldTypeRequiredDescription
contractstringYesThe event contract this channel carries, in <events-artifact-id>/<contract-id> form, e.g. "order-events/order-placed".
addressstringYesThe broker-specific channel address, e.g. a Kafka topic name. Opaque to the wiring config itself — meaning is entirely up to the transport/broker in use.

Id character set

Both node ids and connection ids are restricted to:

[A-Za-z0-9._-]+

i.e. letters, digits, ., _, and -. This set is deliberately conservative — it’s Kafka’s own topic-name character set, and it stays safe unencoded in HTTP header values and URL path segments, both of which a transport implementation is free to use to propagate an id.

Connections

A connection declares how one node calls another — or how an external caller reaches into the topology.

connections:
  - id: "gateway-to-calculator"
    caller:
      nodeId: "gatewayNode"
      failureSemantics:
        id: built-in
        timeout: 2s
        handleTimeout: true
        absoluteTimeout: 10s
        maxRetry: 3
        params:
          waitDuration: 500ms
    callee:
      nodeId: "calculatorNode"
      authorization:
        id: rule-table
        params:
          allow: "shout"
          deny: "whisper"
    transport:
      id: http
      handleTimeout: true
      params:
        host: "${CALC_HOST:-localhost}"
        port: "${CALC_PORT:-8081}"
    serializer:
      id: json
      params:
        schemaRegistryUrl: "${SCHEMA_REGISTRY_URL:-http://localhost:8081}"
    authentication:
      id: shared-secret
      params:
        secret: "${GATEWAY_SECRET}"

Top-level connection fields

FieldTypeRequiredDescription
idstringYesThis connection’s own identifier — distinct from the node references inside caller/callee. Must be unique across the entire wiring configuration. Same character set as node ids.
calleeCallee blockYesThe called node (callee.nodeId), and optionally callee-side plugin configuration.
callerCaller blockNoThe calling node (caller.nodeId), and optionally caller-side plugin configuration. Absent means the caller is external to the Itara topology.
transportTransportEntryYes, resolvable at connection-level or per-sideThe transport this connection uses.
serializerSerializerEntryConditionally — see belowThe serializer this connection uses. Required for every connection except direct ones; resolvable at connection-level or per-side.
authenticationAuthenticationEntryNoAuthentication mechanism. Valid at connection-level, caller-side, or callee-side. Absent means noop.

A connection with no caller block is an external connection — it represents traffic entering the topology from outside (e.g. a public HTTP endpoint), rather than a call between two nodes both managed by Itara.

A connection whose transport.id is (case-insensitively) "direct" is a direct connection — an in-process, colocated call. Direct connections never cross a process boundary, so nothing on them is ever serialized, and they don’t require a serializer block. A direct connection cannot be external — an in-process call has no meaning without a caller also managed by Itara.

Caller and callee blocks

callee:
  nodeId: "calculatorNode"
  authorization:
    id: rule-table
    params: { allow: "shout", deny: "whisper" }

caller:
  nodeId: "gatewayNode"
  failureSemantics:
    id: built-in
    maxRetry: 3

Each block may additionally hold transport, serializer, and authentication configuration, independent of the connection level. failureSemantics is valid on caller only; authorization is valid on callee only.

FieldTypeRequiredDescription
nodeIdstringYes within its blockThe node this side refers to.

Override happens per plugin kind, not per side as a whole — a caller block overriding just transport while serializer still falls back to connection-level is valid. Where a side-specific declaration for a given plugin kind is present, it replaces the connection-level declaration of that kind for that side entirely — block-level, not field-level; nothing merges.

Transport block

Valid at connection-level, caller-side, or callee-side (see Caller and callee blocks).

transport:
  id: http
  handleTimeout: true
  params:
    host: "${CALC_HOST:-localhost}"
    port: "8081"
FieldTypeRequiredDescription
idstringYesThe transport type identifier. Must match the artifact.id of a kind = "transport" metadata file discoverable by the agent — except for the special value direct, which selects the built-in in-process transport and requires no corresponding artifact.
handleTimeoutbooleanNo — defaults to falseWhether the transport should enforce the per-attempt timeout natively for this connection. Only meaningful if the transport’s own metadata declares the matching capability; the actual timeout value lives in failureSemantics.timeout, not here.
paramsmap of string → scalarNo — defaults to {}Transport-specific connection parameters, passed through to the transport implementation as-is. The wiring config has no schema for this map and no knowledge of what any particular transport expects. Values must be scalars (string, number, or boolean); nested maps or lists are not valid here.

Serializer block

Valid at connection-level, caller-side, or callee-side.

serializer:
  id: json
  params:
    schemaRegistryUrl: "${SCHEMA_REGISTRY_URL:-http://localhost:8081}"
FieldTypeRequiredDescription
idstringYes (see connection-level rule)The serializer id. Must match the artifact.id of a kind = "serializer" metadata file discoverable by the agent.
paramsmap of string → scalarNo — defaults to {}Serializer-specific parameters, passed through as-is. Same shape and same “no schema” property as transport.params.

The block (and its id in particular) is required on every connection except direct ones — there is no serializer choice that’s safe to assume silently for a connection that crosses a process boundary.

failureSemantics block

Valid on the caller block only — retry/timeout is exclusively the caller’s concern.

failureSemantics:
  id: built-in
  timeout: 2s
  handleTimeout: true
  absoluteTimeout: 10s
  maxRetry: 3
  params:
    waitDuration: 500ms
FieldTypeRequiredDescription
idstringNo — defaults to noopThe failure-semantics implementation id. Must match the artifact.id of a kind = "failure-semantics" metadata file, unless left at the noop default.
timeoutduration string, e.g. "2s", "500ms"NoPer-attempt timeout.
handleTimeoutbooleanNo — defaults to falseWhether the failure-semantics implementation should enforce the per-attempt timeout by externally interrupting the transport thread. Only meaningful if the implementation’s own metadata declares support for it.
absoluteTimeoutduration string, e.g. "10s"NoHard ceiling on total execution time across all attempts, including retries.
maxRetryintegerNoMaximum number of retries. Total attempts made = maxRetry + 1.
paramsmap of string → scalarNo — defaults to {}Implementation-specific parameters (e.g. backoff configuration), passed through as-is.

The block as a whole is optional; omitting it entirely means the noop implementation is used — no retries, no timeout enforcement, no circuit breaking.

authentication block

Valid at connection-level, caller-side, or callee-side.

authentication:
  id: shared-secret
  params:
    secret: "${GATEWAY_SECRET}"
FieldTypeRequiredDescription
idstringNo — defaults to noopThe authentication mechanism id. Must match the artifact.id of a kind = "authentication" metadata file, unless left at the noop default. If the block is present, id may not be blank — omit the block entirely to get the noop default instead of supplying an empty id.
paramsmap of string → scalarNo — defaults to {}Implementation-specific parameters, passed through as-is.

authorization block

Valid on the callee block only — authorization is exclusively the callee’s own access-control decision.

authorization:
  id: rule-table
  params:
    allow: "shout"
    deny: "whisper"
FieldTypeRequiredDescription
idstringNo — defaults to noopThe authorization mechanism id. Must match the artifact.id of a kind = "authorization" metadata file, unless left at the noop default. Same non-blank rule as authentication when the block is present.
paramsmap of string → scalarNo — defaults to {}Implementation-specific parameters, passed through as-is.

Full example

nodes:
  - id: "order-service-node"
    component: "order-service"
  - id: "pricing-service-node"
    component: "pricing-service"
  - id: "orderPlacedChannel"
    kind: virtual
    contract: "order-events/order-placed"
    address: "demo.events.order-placed"
 
connections:
  # In-process call — order-service calls pricing-service directly.
  - id: "order-to-pricing"
    caller:
      nodeId: "order-service-node"
    callee:
      nodeId: "pricing-service-node"
    transport:
      id: direct
 
  # Inbound HTTP entry point — no 'caller' block, so the caller is external.
  - id: "gateway-to-order"
    callee:
      nodeId: "order-service-node"
    transport:
      id: http
      params:
        host: "${ORDER_HOST:-localhost}"
        port: "${ORDER_PORT:-8080}"
    serializer:
      id: json
    authentication:
      id: shared-secret
      params:
        secret: "${GATEWAY_SECRET}"
 
  # order-service publishes onto a virtual (broker-backed) channel.
  - id: "order-to-order-placed-channel"
    caller:
      nodeId: "order-service-node"
    caller:
      nodeId: "orderPlacedChannel"
    transport:
      id: kafka
      params:
        bootstrapServers: "${KAFKA_BOOTSTRAP:-localhost:9092}"
    serializer:
      id: protobuf

3.5 - Metadata Format

Identity and properties for anything that takes part in an Itara-managed topology — components, contracts, events, and plugins alike.

A .itara file is a small TOML document shipped alongside every build artifact (a component, an API, a transport, a serializer, an observer, etc). It declares what the artifact is — its identity, the versions it was built against, and (depending on its kind) a handful of kind-specific declarations that other tooling and the agent itself use to wire things up safely.

Unlike the wiring configuration, which is one file describing an entire deployment topology, a .itara file describes exactly one artifact.

This document describes the file format — every section, every field, what’s required, what defaults apply, and which artifact kinds each section is meaningful for.

Wherever a field is described below as a version — artifact.version, api-version, and every version inside a { id, version } entry — it’s a semver string, not an arbitrary one; version-range fields (like [serializers] supported[].version) are semver ranges checked against it.

Forward compatibility

Unknown sections, and unknown fields within known sections, are always silently ignored. This is deliberate: it lets an older agent load an artifact built with a newer version of the metadata schema without failing, simply ignoring whatever it doesn’t understand. Don’t rely on unrecognized fields causing a parse error — they won’t.

Top-level structure

[artifact]
kind = "component"
id = "inventory"
version = "1.0.0"
api-version = "1.x"
 
[runtime]
language = "java"
compiler = "21"
 
[itara]
spec-version = "0.1"
core-version = "0.1+"
SectionRequiredMeaningful forDescription
[artifact]Yesevery kindIdentity: what this artifact is, and its own version.
[runtime]Noevery kindWhat language/compiler built this artifact.
[itara]Noevery kindWhich version of the Itara spec/core this artifact targets.
[methods]NoapiPer-method properties of the contract — currently, which methods are non-idempotent.
[implemented-event-contracts]NocomponentWhich event contracts this component implements.
[contract]Noapi, eventsProperties of the contract as a whole — currently, its message format.
[serializers]NoapiWhich serializers this artifact was compiled with support for.
[serializer]NoserializerWhat this serializer implementation is and can do.
[transport]NotransportWhat this transport implementation is and can do.
[failure-semantics]Nofailure-semanticsWhat this failure-semantics implementation can do.
[authentication]NoauthenticationWhat kind of authentication mechanism this is.
[authorization]NoauthorizationWhat kind of authorization mechanism this is.
[api-dependencies]NocomponentWhich API contracts this component calls, and at what version.

Every section beyond [artifact] is optional at the parser level. The “meaningful for” column describes intent, not an enforced restriction — nothing about the format itself rejects a [transport] section on a component artifact, for instance.

[artifact] (required)

[artifact]
kind = "component"
id = "inventory"
version = "1.0.0"
api-version = "1.x"
FieldTypeRequiredDefaultDescription
kindstringYes—The artifact’s kind: component, api, events, transport, serializer, observer, failure-semantics, authentication, authorization, context-handler.
idstringYes—Component id for component/api/events artifacts (e.g. "inventory"); SPI implementation name for transport/serializer/observer/failure-semantics/authentication/authorization artifacts (e.g. "http").
versionstringNo""This artifact’s own version.
api-versionstringNo""The API version this artifact exposes (for api/component producing an API) or was compiled against.

.itara files are named without an embedded version — identity comes purely from (kind, id).

[runtime] (optional)

[runtime]
language = "java"
compiler = "21"
FieldTypeRequiredDefaultDescription
languagestringNo""Implementation language, e.g. "java", "rust".
compilerstringNo""Compiler/language version used to build the artifact, e.g. "21", "1.78+".

[itara] (optional)

[itara]
spec-version = "0.1"
core-version = "0.1+"
FieldTypeRequiredDefaultDescription
spec-versionstringNo""Version of the Itara spec this artifact was built against.
core-versionstringNo""Version of the Itara core this artifact was built against.

[methods] (optional — api artifacts)

[methods]
non-idempotent = ["divide", "transfer", "placeOrder"]
FieldTypeRequiredDefaultDescription
non-idempotentarray of stringsNo[]Names of contract methods that are not safe to retry automatically.

Absence of this section (or of a given method name within it) means the method is treated as idempotent — the safe default for retry purposes.

[implemented-event-contracts] (optional — component artifacts)

[implemented-event-contracts]
contracts = [
  { id = "order-events/order-placed",    version = "1.0.0" },
  { id = "order-events/order-cancelled", version = "1.0.0" },
]
FieldTypeRequiredDefaultDescription
contractsarray of { id, version }No[]Event contracts this component implements.

Each entry:

FieldTypeRequiredDescription
idstringYesFull contract reference: <events-artifact-id>/<contract-id>, e.g. "order-events/order-placed".
versionstringYesVersion of the events artifact this implementation was written against.

Present only on component artifacts that consume event contracts.

[contract] (optional — api and events artifacts)

[contract]
message-format = "protobuf"
FieldTypeRequiredDefaultDescription
message-formatstringNo""The message format the contract’s method parameter/return types (for api) or event payload types (for events) are generated from, e.g. "protobuf". An empty string is treated identically to the section being absent entirely — both mean the contract uses plain, hand-written types rather than a generated structural format.

This is a structural property of the contract’s own types — it’s unrelated to which serializer ids an artifact is compatible with (that’s [serializers], below); message format and serializer choice vary independently.

[serializers] (optional — api artifacts)

[serializers]
supported = [
  { id = "json", version = "1.x" },
  { id = "protobuf", version = "1.x" },
]
FieldTypeRequiredDefaultDescription
supportedarray of { id, version }No[]Serializers this API artifact was compiled with support for.

Each entry:

FieldTypeRequiredDescription
idstringYesMatches a serializer’s artifact.id, e.g. "json", "protobuf".
versionstringYesA semver range checked against that serializer’s own artifact.version.

Not every language needs this section populated. Java loads serializers as runtime plugins, so an API artifact can work with a serializer it never declared here. Rust has no equivalent runtime plugin mechanism — a Rust API artifact must be compiled with support for whichever serializers it will use, so [serializers] supported is how that compiled-in support gets declared and checked.

[serializer] (optional — serializer artifacts)

[serializer]
type = "protobuf"
 
[serializer.capabilities]
message-formats = ["protobuf"]
FieldTypeRequiredDefaultDescription
typestringNoabsent/nullThe serialization category this implementation handles — e.g. "json", "protobuf". Distinct from artifact.id, which is the unique identifier of the specific implementation.
capabilitiesSerializerCapabilitiesNosee belowSee below.

SerializerCapabilities

FieldTypeRequiredDefaultDescription
message-formatsarray of stringsNo[]Structural message formats this serializer implementation can handle generically, beyond plain hand-written types — e.g. a protobuf serializer declares ["protobuf"] here, meaning it can handle proto-generated types via reflection.

Defaults to an empty list when the [serializer.capabilities] section is absent — a serializer is assumed to handle plain types only, until it explicitly declares a structural format it supports. This has no bearing on error-payload handling, which every serializer supports unconditionally regardless of its declared message-formats.

[transport] (optional — transport artifacts)

[transport]
type = "http"
 
[transport.capabilities]
native-call-timeout = true
externally-interruptible = true
FieldTypeRequiredDefaultDescription
typestringNoabsent/nullThe transport category / communication protocol, e.g. "http", "kafka", "amqp". Two implementations sharing the same type are considered compatible caller/callee pairs. Distinct from artifact.id.
capabilitiesTransportCapabilitiesNosee belowSee below.

TransportCapabilities

FieldTypeRequiredDefaultDescription
native-call-timeoutbooleanNotrueWhether the transport can abort an in-flight call natively when the per-call timeout expires. If false, the failure-semantics layer is responsible for enforcing the timeout externally instead.
externally-interruptiblebooleanNotrueWhether it’s safe to externally interrupt the thread blocked on this transport’s send call — i.e. doing so won’t leave the transport in a broken state.

Both fields default to true when the [transport.capabilities] section is absent entirely — a transport is assumed capable unless it explicitly declares otherwise. This is the opposite default from SerializerCapabilities and FailureSemanticsCapabilities, both of which default to the unsupported/empty case.

[failure-semantics] (optional — failure-semantics artifacts)

[failure-semantics]
 
[failure-semantics.capabilities]
supports-external-timeout = true
FieldTypeRequiredDefaultDescription
capabilitiesFailureSemanticsCapabilitiesNosee belowSee below.

FailureSemanticsCapabilities

FieldTypeRequiredDefaultDescription
supports-external-timeoutbooleanNofalseWhether this failure-semantics implementation can enforce the per-attempt timeout by externally interrupting the transport thread. Implementations must opt in explicitly.

[authentication] (optional — authentication artifacts)

[authentication]
type = "mtls"
FieldTypeRequiredDefaultDescription
typestringNoabsent/nullThe authentication mechanism category, e.g. "shared-secret", "mtls", "jwt". Distinct from artifact.id.

[authorization] (optional — authorization artifacts)

[authorization]
type = "rbac"
FieldTypeRequiredDefaultDescription
typestringNoabsent/nullThe authorization mechanism category, e.g. "rbac", "rule-table", "opa". Distinct from artifact.id.

[api-dependencies] (optional — component artifacts)

[api-dependencies]
calls = [
  { id = "calculator", version = "1.0.0" },
  { id = "inventory",  version = "2.1.0" },
]
FieldTypeRequiredDefaultDescription
callsarray of { id, version }No[]Synchronous API contracts this component calls.

Each entry:

FieldTypeRequiredDescription
idstringYesMatches the artifact.id of the callee’s kind = "api" metadata file.
versionstringYesThe exact version this component was compiled against — used to check compatibility against the callee’s declared api-version.

Absence of the section (rather than an empty calls list) means the same thing: the component declares no outbound API calls. This is valid, and expected, for leaf components.

Full example

A component that calls one API and implements two event contracts:

[artifact]
kind = "component"
id = "order-service"
version = "1.4.0"
api-version = "1.x"
 
[runtime]
language = "java"
compiler = "21"
 
[itara]
spec-version = "0.1"
core-version = "0.2"
 
[api-dependencies]
calls = [
  { id = "pricing", version = "2.0.1" },
]
 
[implemented-event-contracts]
contracts = [
  { id = "order-events/order-placed",    version = "1.0.0" },
  { id = "order-events/order-cancelled", version = "1.0.0" },
]

An API artifact declaring one non-idempotent method:

[artifact]
kind = "api"
id = "order-service"
version = "1.4.0"

[methods]
non-idempotent = ["placeOrder"]

An HTTP transport artifact:

[artifact]
kind = "transport"
id = "http"
version = "0.3.0"
 
[runtime]
language = "rust"
compiler = "1.78+"
 
[transport]
type = "http"
 
[transport.capabilities]
native-call-timeout = true
externally-interruptible = true

4 - Explanation

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

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

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

5 - FAQ & Comparisons

How Itara relates to service meshes, ESBs, hexagonal architecture, and other things people reach for first.

What is Itara?

Itara is a specification for topology-as-configuration, with reference implementations in Java and Rust. The core idea: which components exist, how they communicate, and where they run are configuration decisions — not code decisions. Component logic expresses what the system does. The wiring config expresses how the parts connect. These are different concerns and they live in different places.

In practice: you write a plain interface and an implementation. The agent reads the wiring config at startup, wires the components together — directly in-process, over HTTP, or over any other supported transport — and hands off to your application. Your code never changes when the topology changes. Only the config changes.


Is this a framework? A runtime? A service mesh?

None of those exactly. It is closer to a specification with reference implementations. The Java and Rust agents are engines that read the spec and apply it at startup. Your application compiles and runs as a normal binary. There is no Itara API in your business logic, no annotations on your domain objects, no framework base classes to extend.

The closest analogy is a classloader for distributed systems topology — something that wires things together at startup and then gets out of the way.


What is Itara not trying to do?

Itara is a thin slice — it concentrates one specific concern, topology, into an explicit and executable layer. It is not trying to replace anything in the existing stack.

Container orchestrators (Kubernetes, Nomad, ECS) own deployment, scaling, and scheduling. Itara declares which components connect and how; orchestrators decide where they run and how many instances exist. These are complementary concerns.

Service meshes (Istio, Linkerd, Envoy) operate on network traffic between deployment units, typically as sidecars. Itara operates at the component level, above the network, with explicit contracts between components. A service mesh and Itara can coexist — they solve different problems at different layers.

Dapr provides building blocks (pub/sub, state stores, service invocation) as a sidecar API. Itara declares and realizes the communication structure between components without a sidecar in the call path at runtime. See the Dapr entry below for a longer comparison.

Deployment and CI/CD tooling (Terraform, Helm, ArgoCD) own how software gets to where it runs. Itara’s tooling will eventually generate inputs for these systems from the declared topology, but it does not replace them.

Observability platforms (Datadog, Jaeger, Grafana, Kibana) own how telemetry is stored, queried, and visualised. Itara fires structured observability events at every component boundary through a pluggable observer SPI — any observability stack implementing it can consume them. Where that data goes is entirely the operator’s choice.

The goal is not to own the stack. It is to make one currently invisible layer — topology — explicit, verifiable, and executable, and let everything else continue doing what it already does well.


Isn’t this the same as CORBA, Java RMI, or other transparency-oriented systems?

No, and the distinction matters.

CORBA, Java RMI, DCOM, and EJB all attempted to make remote calls indistinguishable from local calls — to hide the network from the developer entirely. The premise was that the network doesn’t exist, or at least that you shouldn’t have to think about it. This failed for well-documented reasons: the network does exist, it has latency and failure modes that local calls don’t, and hiding those differences produces systems that are hard to reason about and harder to debug.

Itara’s premise is the opposite. The network exists and its presence is always visible — in the wiring config, in the traces, in the error contracts. What Itara removes from component code is not the awareness that a call might be remote, but the responsibility for encoding how that remote call works. Failure semantics, serialization, transport selection, context propagation — these are topology concerns declared in the wiring config, not infrastructure boilerplate written into the business logic.

The difference in plain terms: CORBA made topology invisible. Itara makes it explicit and concentrated. Topology is declared, validated by tooling before deployment, and visible to everyone — the opposite of hidden.

The fact that this keeps getting attempted — CORBA, RMI, EJB, actor frameworks — is itself a signal the underlying problem is real, not a reason to dismiss another attempt. What’s different here isn’t the ambition, it’s the mechanism.


Isn’t this just hexagonal architecture, DDD, and dependency injection?

Hexagonal architecture and DDD answer how to organize the internals of a component — its ports, its domain model, its bounded context. Itara doesn’t compete with either; a well-designed hexagonal component is exactly the kind of thing that plugs cleanly into Itara’s contract model.

The closer comparison is Spring Modulith, which does let modules communicate with configurable transport (in-memory or Kafka via event externalization) independent of business logic. That’s real, adjacent territory. Two differences matter: Modulith’s communication config lives inside the monolith’s own codebase, coupled to annotations on the module/event itself — it has no model of independently deployed, independently versioned services. And it governs modules within one JVM deployable; it can’t express “this module might become its own process later” without someone picking transport code at that point. Itara’s wiring config is external to every component’s code and treats placement — colocated or distributed — as a topology decision, not a modularity pattern chosen once at build time.

Dependency injection frameworks (Spring, Guice) wire dependencies within one process; they have no concept of a dependency living in a different process at all, which is the actual problem Itara is aimed at.


Isn’t this just workflow orchestration, like the Arazzo Specification?

No — different layer entirely. Arazzo describes sequences of calls to already-existing, already-deployed APIs to accomplish something: call this endpoint, take its output, feed it into that one, in this order. It assumes the APIs already exist as separate, deployed services and describes the choreography of calling them.

Itara doesn’t describe business workflows — it describes the structure of the system those workflows run on top of: which components exist, how they connect, whether they’re even separate processes at all. Arazzo has no concept of placement, transport, or whether two things need to be separate services in the first place — it’s closer to a very structured API-calling script than to a topology declaration.


Isn’t this just Spring Integration (or another Enterprise Integration Patterns library)?

Closer than hexagonal/DDD, and worth taking seriously. Spring Integration does let you swap an in-memory channel for JMS/AMQP/Kafka without touching endpoint logic — real overlap.

The difference: Spring Integration’s channels are still part of your application’s code and config. You write IntegrationFlow beans, your handler methods take Message<T>, the framework is present in the business layer, not just at a boundary outside it. It’s also fundamentally single-application scoped — bridging to an independently deployed, independently versioned service is an adapter you configure by hand, not a topology decision validated across two separately built artifacts before deploy. And there’s no cross-language story; it’s Spring, full stop.

One line version: Spring Integration decouples channels within one application. Itara decouples topology across independently built, independently deployed, potentially different-language components — and validates the connection before either side ships.


How is this different from Dapr?

The difference is where the abstraction sits.

Dapr is a sidecar: a separate process injected alongside your container. Every call your service makes to another service goes through a localhost network hop to the Dapr sidecar, which routes it to the target’s sidecar, which delivers it. This is true even when both services are on the same host. The sidecar tax is always paid.

Itara works at the method call level. When two components are declared colocated in the wiring config, the proxy resolves at startup and the call is a direct in-process function call — no serialization, no network, no sidecar. When they are remote, the call goes over HTTP or whichever transport is configured. The code is identical in both cases. Dapr cannot offer zero-overhead colocation because the sidecar model structurally prevents it.

There is also a granularity difference. Dapr operates at the service level — the unit of deployment is a service, and the sidecar wires services together. Itara operates at the component level, which is deliberately finer-grained than a service. Multiple components can be colocated in one process or distributed across separate processes — all through config changes. Service boundaries are not fixed deployment artifacts in Itara; they are adjustable topology decisions.

The other difference: Dapr abstracts infrastructure (state stores, brokers, secrets). Itara abstracts topology — where components run and how they connect. These are different problems.


How does Itara compare to Microsoft Aspire?

They operate at different layers and solve different problems.

Aspire is a development and deployment orchestration tool — it declares which services and infrastructure resources exist, how they start up, and how they get deployed. It significantly improves the experience of running and shipping multi-service applications, particularly for .NET teams.

Itara operates one layer deeper: the communication contracts between components. It declares what components call, what they return, how failures are handled, and validates that every connection is correct before deployment. Where Aspire answers “how do I run and deploy my services consistently,” Itara answers “what are the communication contracts between my components, are they valid, and can topology change without touching code.” The two tools address complementary concerns and can coexist.


What does zero-overhead colocation actually mean?

When two components share the same process and type system — two JVM components wired as direct, or two Rust components in the same process — the proxy calls the implementation directly. Nothing happens that isn’t strictly necessary for the call itself: no serialization, no network hop, no transport overhead of any kind.

That’s not the same as saying nothing happens at all. Observability events still fire. Authentication and authorization still run, if configured for that connection. Component scope is still established. Whatever else the topology layer legitimately needs to do for a call to be correct and safe still happens — the guarantee is that none of it is transport-shaped work a colocated call never needed in the first place, not that the call is literally free.

The corollary: running two components colocated in Itara costs nothing beyond what a monolith would already cost you for the equivalent call. The boundary itself is free. This is what makes topology switching meaningful — moving from colocated to remote changes what shows up in your traces as network and serialization cost, without changing the topology-layer guarantees around the call.


If two components are colocated, do they trust each other?

No — colocation is a placement decision, made to optimise communication overhead, not a trust decision. Every executing component has an agent-established scope defining exactly what it can reach, and that boundary is enforced identically whether a connection is colocated or crosses a process. A component cannot supply its own identity, and it cannot reach another component it has no declared connection to — regardless of whether they happen to share a process. This is what makes it safe to colocate independently built components for performance without also having to trust them with unrestricted access to each other.

Authentication and authorization are pluggable SPIs on top of this, opt-in per connection, and — same as the scope guarantee underneath them — behave identically whether the connection is colocated or remote.


Topology is no longer in the component code — how do I handle network failures?

Topology is not hidden — it is explicitly declared in the wiring config, where it is visible, auditable, and validated before deployment. What is absent from component code is the responsibility for encoding how failures travel, not the awareness that failures can happen.

Failure semantics — retries, timeouts, circuit breaking — are connection-level configuration, not component-level code. They belong in the wiring config alongside the transport declaration. The component code never sees infrastructure boilerplate, but the failure contracts are explicit and declared.

The failure semantics SPI is pluggable. A single implementation owns the complete strategy for a connection — retry logic, timeout enforcement, circuit breaking — as a cohesive unit of behaviour declared in configuration. A built-in implementation ships with the platform covering the common cases. Teams with specific requirements can provide their own.

Failures surface as typed error contracts at the call site: CHECKED for declared component errors, RUNTIME for unexpected component failures, TRANSPORT for infrastructure failures, and PERMISSION for a caller that wasn’t authenticated or authorized. All four reconstruct as the same exception type on the caller side — one thing to catch, regardless of which kind of failure it was.


With topology out of the component code, doesn’t that make debugging harder?

With topology being more visible than ever before, tracking down errors actually becomes easier.

At the code level, every failure that crosses a component boundary surfaces as a typed error contract, carrying the original exception class and message where applicable. Without reading a line of transport code, you know immediately whether the failure was in the business logic, the component implementation, the infrastructure layer, or a permission problem.

At the system level, the four-event observability model fires at every component boundary regardless of transport. Every failed request leaves a trace across the full call chain. The trace shows exactly where the failure occurred, with serialization cost, network cost, and component processing time separately measurable.


Does this replace DDD, event sourcing, or other architectural patterns?

No. Itara does not replace architecture, it concentrates it.

You still design your bounded contexts, aggregates, and domain events exactly as before. What changes is where the plumbing lives. Instead of transport configuration, retry policies, and service discovery calls scattered across every service, the communication structure of your system is expressed in one place. The patterns are still yours. The topology that gives them shape is now auditable and changeable without touching code.


Isn’t the wiring config just a configuration file? What makes it architecture?

Every system design starts as a diagram: boxes for components, arrows for connections. That diagram is authoritative for exactly as long as it takes someone to start writing code. The moment implementation begins, the diagram becomes a record of an intention, and the code — scattered across HTTP clients, service discovery calls, retry policies, timeout settings — becomes the real, load-bearing truth, whether or not it still agrees with the diagram. Nothing enforces the relationship between the two going forward, which is exactly why architecture diagrams reliably go stale and why understanding a real system means reading all the code, not looking at the drawing that was supposed to describe it.

The wiring config is not configuration in the sense of database connection strings or feature flags — values that tune an already-correct system. It’s the design, kept alive. The agent doesn’t consult it as a reference; it enforces it — an application has no choice but to follow the declared topology, or it doesn’t start. Every connection, every deployment boundary, every transport choice is declared there and nowhere else, and the tooling validates it before anything deploys.

The difference from a diagram, in one sentence: a diagram can silently stop being true. The wiring config can’t — the system has no way to run without agreeing with it.


Why not just write standard HTTP or gRPC clients?

Because the moment you write an HTTP client into a service you make an architectural commitment that is expensive to reverse. Splitting the service later means refactoring the client. Changing which request-reply transport carries the call — HTTP to gRPC, say — means touching both sides.

Itara makes that specific decision a configuration entry. Swapping the transport under a request-reply connection — HTTP for gRPC, or for a request-reply mechanism over Kafka — is a wiring config change; the component code never changes because it never knew which transport was carrying the call.

This is not a claim that every communication pattern is interchangeable with every other. Moving to genuinely event-driven, publish-subscribe communication is a different exchange pattern, not a transport swap — it changes who knows about whom (a producer doesn’t know its consumers), how the APIs need to be written, and how it’s expressed in the wiring config as a distinct topology shape (a virtual node), not a transport parameter on an existing connection. Itara makes topology decisions explicit and reversible; it doesn’t make every semantic shape equivalent to every other one.


Does this work with Spring Boot?

Yes. Itara and Spring Boot are different layers and coexist naturally — neither knows about the other. A Spring @Bean method can call ComponentLookup to fetch a component implementation and return it as a bean. Spring manages its context, Itara manages the wiring beneath it. There is no dedicated adapter at this stage — Itara and Spring Boot coexist, but there is no deeper integration.

Migrating an existing Spring Boot service is incremental: extract the service interface into a separate API artifact, write an activator (a factory method that returns the implementation), and update the relevant @Bean method to pull from ComponentLookup instead of constructing directly. The rest of the Spring context is untouched. Deeper integration — for example reusing Spring’s servlet infrastructure for HTTP transport — may happen through optional separate libraries in the future.


Why was Rust chosen as the second implementation language?

Two reasons, and they are related.

The first is practical: Rust is the planned implementation language for Itara’s tooling — the CLI, the deployment tool, and eventually the controller. A working Rust component implementation means the tooling can be tested against real artifacts from day one.

The second is philosophical. Rust’s central value is that correctness should be a compile-time property — errors caught before the program runs, not at runtime when something breaks in production. Itara’s central value for topology is the same thing expressed at the system level. The .itara metadata files that every component artifact carries — declared serializer support, contract version information, message format — exist specifically so that tooling can validate the entire system topology before anything deploys. A serializer mismatch, a missing component, an incompatible contract version: these are caught at configuration time, not discovered when the first request fails.

The parallel is genuine, not coincidental. A language community that thinks carefully about compile-time correctness is the right audience for a framework that extends that philosophy to distributed system topology.


What languages are supported?

Java and Rust today, at different levels of maturity. Java has the full feature set — message formats, pluggable authentication and authorization, classloader isolation, everything described elsewhere in this FAQ. Rust is current for what the tooling itself needs — the CLI is a Rust project, and cross-language calls with Java work correctly today, demonstrated int he demo and examples, with a Java component calling a Rust component over HTTP producing a single distributed trace in Kibana. Beyond that, the Rust component/agent implementation is closer to proof-of-concept: it hasn’t caught up to Java on more recently shipped capabilities. Go and Python are on the roadmap. The specification is language-neutral; any language with sufficient metaprogramming or build-time automation capability can implement it.


What happens if two colocated components have conflicting dependency versions?

Most of the time, this is solved structurally rather than detected and warned about. Independently developed Java components with conflicting transitive dependencies can be colocated in the same process via a direct connection, with no code changes to either. Each component’s dependencies are isolated in their own classloader; the shared, common types (contracts, Itara internals) are guaranteed a single class identity across all of them. Two components can depend on different, incompatible versions of most libraries and colocate without conflict, because neither ever sees the other’s dependency tree.

The real exception is anything that’s genuinely a JVM-global singleton rather than an ordinary library — an embedded server (Tomcat and similar), or a logging framework that resolves its own configuration via the thread context classloader (Log4j2, Logback). Two components each trying to initialize their own copy of one of these can conflict regardless of classloader isolation, since the resource they’re contending over isn’t per-classloader to begin with. These need deliberate placement in the shared classloader rather than being left to isolate automatically — isolation removes the common case of conflict, it doesn’t remove the need to think about genuinely global state.

Isolated and shared classloader strategies are selectable per deployment — components designed together that already share a dependency strategy can still opt into the simpler shared model.


How do I handle secrets in the wiring config?

The wiring config supports environment variable substitution — any value can be written as ${VAR_NAME} and the agent resolves it at startup from the environment. This is sufficient for most cases: hostnames, ports, bootstrap server addresses, credentials, and similar connection parameters can all be injected without hardcoding them.

More sophisticated secret management — a secret store SPI that can resolve secrets from Vault, AWS Secrets Manager, or similar — is a natural future extension. The substitution pass that already exists provides a clean insertion point for it. For now, env var substitution is the supported and recommended approach.


Can I use Itara without the CLI?

Yes. The CLI is not required to run components — the agent reads the wiring config directly at startup and works without it.

What you lose without the CLI is the safety layer: orphaned nodes, version mismatches, connection and node id violations, message-format/serializer compatibility, timeout misconfiguration, and transport capability conflicts are all caught by itara verify before deployment. Without it, these surface at startup or at runtime instead. The CLI is not optional polish — it is how Itara keeps its promise that incorrect topologies cannot be deployed silently. The agent alone cannot substitute for it.


What happens if the wiring config is wrong?

Two layers of protection catch configuration errors.

The first is the CLI: itara verify catches structural errors and compatibility mismatches before anything starts. The set of checks grows with the project and with feedback from real usage.

The second is the agent itself: whatever the CLI does not catch, the agent validates at startup time. If a connection cannot be resolved, a referenced artifact is missing, or a configuration is invalid, the agent fails fast and the application does not start. The deployment never succeeds in a broken state. There is no partial startup, no lazy failure on the first call.


How does Itara interact with service discovery?

It doesn’t, currently. Itara relies on the infrastructure’s ability to resolve the addresses declared in the wiring config — whether that’s DNS, a hosts file, or a container orchestrator’s internal networking. The address in the config must be resolvable by whatever mechanism the environment provides.

A service discovery SPI is on the roadmap, allowing implementations to resolve component addresses dynamically at startup rather than requiring them to be statically declared in the wiring config. It’s a smaller priority right now than adoption-focused work — getting-started documentation, authoring tooling, and a versioning story — since those matter more for anyone trying Itara for the first time.


How do I handle schema evolution in event contracts?

Nothing special is implemented at this stage. The usual approaches apply: additive changes are safe, breaking changes require coordinated versioning across producers and consumers. The event contract version declared in the .itara metadata file is what the CLI uses to check compatibility — a version bump signals that consumers need to be reviewed.

Patterns like consumer-driven contract testing and the expand-contract technique work alongside Itara without any special support. This is an area that will develop further as real-world usage surfaces what tooling is actually needed.


Should the wiring config be version-controlled?

Yes — version controlling the wiring config is the recommended approach. It is the authoritative declaration of the system’s topology, and treating it with the same discipline as code is the right instinct.

How topology changes are managed across environments, how config versions map to deployments, and how rollback is handled are not yet fully specified. This is active design work, not an afterthought — it’s a prerequisite for closed-loop drift detection (comparing what’s declared against what’s actually running), which is on the roadmap. Feedback on real-world config management needs is welcome.


What does migration away from Itara look like?

Components are plain classes and interfaces. The API artifacts are plain Java interfaces or equivalent. Nothing in the component code requires Itara to compile or run — the only Itara dependency is in the activator, a factory method that wires the component into the topology.

Migrating out means replacing the Itara wiring with whatever mechanism you want to use instead, and removing the activator. The business logic, the contracts, and the tests are all untouched.

Migration out can also be gradual, the same way migration in can be. Itara coexists with other communication mechanisms without interference — you can move connections out of the wiring config one at a time while the rest of the system continues running through Itara unchanged.


Where does the name come from?

Itara means “of the other” or “belonging to another” in Sanskrit — which fits rather well, since the whole point is that topology belongs to configuration, not to the component. The component does not own its connections.

That said, the honest answer is that the core team is not good at naming things. The name came from browsing fantasy, anime, manga, and sci-fi references, merging two names that sounded reasonable together, and checking that the result was not already overused on GitHub. It turned out to also mean something relevant in Sanskrit. That part was a lucky accident.