Across binaries
Coming from Go? Splitting a program into services usually means rewriting in-process calls as RPC or queue clients. In Hale the publisher and subscriber code doesn’t change — a topic that was an in-process queue becomes a Unix socket or a broker by adding one line to
main’sbindings { }block. The deployment seam is the only place that knows.
A topic is in-process by default
Section titled “A topic is in-process by default”When a topic isn’t mentioned in any bindings { } block, it’s
delivered by an in-process cooperative queue. Two loci in the
same binary just talk. Nothing to configure.
Binding a topic to a transport
Section titled “Binding a topic to a transport”To carry a topic between binaries, name it in the main
locus’s bindings { } block with a transport:
main locus App { bindings { MatchReady: unix("/tmp/matches.sock"); } run() { Matchmaker { target_size: 4 }; }}bindings { } is legal only on a main locus. The publisher’s
MatchReady <- info; and the subscriber’s subscribe MatchReady as ... are unchanged — they don’t know or care that delivery
now crosses a socket. The same locus source runs in a test
(in-memory), a single binary (in-memory), and a multi-binary
deployment (unix), chosen entirely at this seam.
The transports that ship
Section titled “The transports that ship”- In-process — the default; absence of a binding.
unix("/path")— an AF_UNIX framed-byte transport, owned by the runtime. The role (listen vs connect) is inferred from whether the binary publishes or subscribes the topic; specifyrole: listen | connectwhen one binary does both.udp://host:port— datagram transport, including IPv4 multicast. Lossy by nature — right for tick streams and telemetry where stale-is-worthless.- A user adapter — any locus you write that satisfies the
__StdBusAdapterinterface (a singlesend(subject, bytes)method). This is how NATS, MQTT, a raw-TCP framing, or a custom JSON-over-WebSocket transport plug in — as ordinary loci in your code, not language features:bindings {BrokerEvt: MyNatsAdapter { url: "nats://prod:4222" };}
The substrate stays neutral on protocol semantics — reliability, ordering, retries, backpressure all live in the adapter body, where they belong.
Talking to other languages: codecs
Section titled “Talking to other languages: codecs”By default the bus uses Hale’s internal wire format, which is
fine Hale-to-Hale but opaque to a consumer in another language.
When you need JSON over a socket or protobuf to a Python peer, a
binding names a codec — a locus that owns encode/decode:
bindings { Tick: unix("/tmp/ticks.sock") codec(TickJsonCodec { });}The codec is structurally typed against the topic’s payload
(encode takes the payload type, decode returns it) and must
be pure — no hidden state — because it runs on transport
threads. Different bindings on the same topic can carry different
codecs; the publisher’s send site doesn’t know which.
The shape this gives you
Section titled “The shape this gives you”A single source tree, decomposed into loci that coordinate over
topics. How those topics are delivered — same process, same
machine over a socket, across the network via a broker — is a
deployment decision living in bindings { }, separate from the
logic. You design the system once and deploy it many ways. The
systems tier adds one more
transport for the highest-frequency same-machine routes:
shared-memory zero-copy.
That’s the services tier: lifecycle, a typed bus, concurrency and placement, supervised parent/child trees, structural failure, and multi-binary deployment. You can build daemons, servers, and distributed systems with this. The final tier goes under the runtime — memory, layout, raw performance, and the C boundary — for when you need that control.
Next: Memory & lifetime.