Skip to content

Connector

Connectors are uni-directional conduits for reliably transmitting data of a particular type. A connector joins exactly one outlet to exactly one inlet.

connector OrderFlow is
  from outlet OrderEventSource.OrderEvents
  to inlet OrderEnricher.RawOrders
with {
  briefly as "Connects the order source to enrichment"
}

Data Transmission Type

A connector transmits one data type. The transmission type is often an alternation of messages, such as the commands and queries an entity might receive.

The outlet's type and the inlet's type must be compatible. A port typed Anything is compatible with every other type — which is what lets the standard module's universal terminators accept any stream.

Port Cardinality

Exactly one connector may attach to any given port.

Fan-in and fan-out are modeled by declaring multiple ports — the arity is what derives a merge or split shape — never by attaching several connectors to a single port.

// Correct: a split declares two outlets, each with its own connector
streamlet Router as split is {
  inlet incoming is event Order
  outlet domestic is event Order
  outlet international is event Order
}
connector ToDomestic      is from outlet Router.domestic      to inlet Home.incoming
connector ToInternational is from outlet Router.international to inlet Abroad.incoming

Attaching more than one connector to a port is an Error.

Scope

A connector may be declared in a Context or, when its two ends are in different contexts, in the enclosing Domain.

Placement validation

Both ends are resolved, and their owning contexts and domains compared:

  • Error — the ends resolve to unrelated domains, meaning domains that share no ancestor. A stream edge between two unrelated domains is a failure of domain analysis, not something to wire around. The remedy is structural: if the two really do need to communicate, they belong under a common parent domain, and then they are related.
  • Permitted — ends in related domains, sharing an ancestor domain. Declare the connector in the ancestor they share.
  • Error — a domain-scoped connector whose ends share one context. It is over-scoped; move it into that context.
  • Error — a context-scoped connector whose ends cross contexts. It is under-scoped; promote it to domain scope.
  • CompletenessWarning — a domain-scoped connector that crosses between two different contexts without the persistent option. Durability at a context boundary can be model correctness, not merely a deployment concern. A connector with both ends inside the same external context crosses nothing and is not asked for it.
  • Error — a cross-context connector that reaches past a boundary. Each end must land on the context's own portlet: the source context's outlet, the target context's inlet.
  • Error — a connector that goes around an adaptor. Where a context declares an outbound adaptor toward another, a connector from that context's own outlet into the target bypasses the boundary: stream-connector-bypasses-adaptor, which names the adaptor being skipped. A connector may instead name the adaptor as its endpoint.

These checks are conservative: they only fire when both ends resolve.

Crossing out, and staying in

A context publishes a public API — its message set — while its representations stay private. A cross-context connector wired to a contained entity's inlet binds a peer to that entity's existence and current command set, so the entity can no longer change without breaking a stranger. That is why reaching past the boundary is an Error rather than advice.

Inside a single context, none of this applies: any processor, streamlet or connector may communicate with any other, and a connector may drive a contained entity's inlet directly.

Nothing requires a dedicated sink or source streamlet to make this work. An entity is a streamlet — it may carry its own inlets and outlets and needs nothing from its context to process messages. A context is a streamlet too: given an inlet and handlers, the context is the sink. Crossing out of a context, the context is the source; crossing in, it is the sink.

A processor uses its OWN ports

A message reaches a processor through that processor's inlet — not a sibling's, and not its container's. Likewise it publishes through its own outlet: a projector's inlet does not make an entity reachable, and an entity cannot publish on its context's outlet.

Getting a message out of an entity therefore runs entity outlet → connector → context inlet → handler → context outlet, and the first step is the entity's own outlet. An entity that handles messages but declares no inlet, or emits messages but declares no outlet, draws a CompletenessWarning. A ??? stub is exempt.

Options

Connector options are advice to the translators converting a connector into something else. A connector often plays a large part in a reactive system's resilience, so several are available.

Option Arguments Meaning
persistent 0 Messages are persisted to stable, durable storage, so they survive failure or shutdown
ordered 0 Delivery preserves order
unordered 0 Delivery order is not significant, enabling partitioning and parallelism
at-least-once 0 Each item is delivered one or more times; handlers must be idempotent
at-most-once 0 Each item is delivered zero or one times; loss is possible
exactly-once 0 Each item is delivered precisely once
partitioned 1 Data is partitioned by a key so a consumer group can process it in parallel
circuit-breaker 0–2 Trip the connection when the downstream is failing

persistent

Messages flowing through the connector are persisted to stable, durable storage, so they cannot be lost even if the system fails or shuts down. This arranges a kind of bulkhead that retains published data despite failures on either end.

Removing the burden of persistence is likely to make a connector considerably more performant, since storage latency is no longer involved — which is why it is opt-in rather than the default.

Why ordering is an option when persistence is not

The default is ordered; unordered is permission, not mandate — best-effort unordered delivery, with ordered delivery remaining a conforming implementation. A generator may decline to honour it and still conform, which is exactly why ordering stayed an option.

Persistence and delivery guarantees fail that test: a delivery guarantee is not something a generator may quietly decline. That is the line between an option and an intention.

Delivery semantics

at-least-once requires the implementation to make handling idempotent, so that running an item twice has the same effect as running it once. at-most-once relaxes that to best-effort, which may increase throughput and lower overhead where data loss is not catastrophic — some IoT systems permit it because the next transmission is imminent.

Producers and Consumers

Attached to the ends of connectors are producers and consumers. These are processors that may originate, terminate, or flow data through them, joining two connectors together.

graph LR;
Source --> C1{{Connector 1}} --> Flow --> C2{{Connector 2}} --> Sink

Because each port takes exactly one connector, a processor that must serve several downstream consumers does so by declaring several outlets — becoming a split — rather than by sharing one outlet among several connectors.

Occurs In

Contains

Nothing