A self-orchestrated microservices demo, entirely in PatLang
One orchestrator process spawns three independent PatLang service processes as real, separate OS processes, waits for each to confirm it's genuinely ready (not just "the process started" — its own presence beacon has to be live), discovers what each one actually offers rather than being told in advance, and dispatches real requests purely by matching capability — no hardcoded ports, no hardcoded knowledge of which process does what. Everything used here is infrastructure already built earlier in the same arc (presence, discovery, and non-blocking spawn), combined for the first time into one real, runnable system.
spawns, waits for readiness, discovers, dispatches, shuts down
Why this can't run live in this page
Every other interactive demo on this site runs in the browser, compiled to WebAssembly. This one can't: spawn() launches a real, separate operating-system process, and there is no browser/WASM equivalent of that at all — a WASM module can't start another program on your machine, for the same sandboxing reasons nothing else on the web can either. Running several independent PatLang programs side by side within one browser tab, talking to each other via postMessage instead of real sockets, was considered and rejected for this page specifically — it would be simulating the same idea with different, weaker infrastructure than the real one, not demonstrating the real one. What follows instead is the actual source code and a real, unedited transcript captured running this on a real machine.
The source, in full
One worker executable serving three possible roles (picked via a command-line argument), plus the orchestrator. Nothing here is new machinery — every host function and library call was already built and tested earlier in this arc.
The worker (self_hosting/examples/microservice_worker.patlang)
include "../lib/signal_discovery.patlang"
let args = argv()
let role = args[0]
let task_name = "svc_" + role
# `when` handlers must be at genuine top level -- registering one
# conditionally inside an `if role == ...` block was tried first and
# found NOT to work: it silently never fires, no error, just a dead
# handler. So every handler is registered unconditionally; a given
# process only ever actually receives the signal matching its own role.
when quit do
set_var("should_stop", "1")
signal_reply("bye")
end
when greet do
signal_reply("Hello, " + event_data + "! (from the greeter microservice)")
end
when add do
let parts = sd_split(event_data, ",")
signal_reply("" + (to_num(str_trim(parts[0])) + to_num(str_trim(parts[1]))))
end
when multiply do
let parts = sd_split(event_data, ",")
signal_reply("" + (to_num(str_trim(parts[0])) * to_num(str_trim(parts[1]))))
end
when time do
signal_reply("current server time: " + now_ms() + "ms since epoch")
end
let port_id = signal_claim(0)
if port_id >= 0 then
if role == "greeter" then
signal_announce(task_name, port_id, [["greet", "greets whoever you send, payload = a name"]])
else
if role == "calculator" then
signal_announce(task_name, port_id, [["add", "adds two numbers, payload = 'a,b'"], ["multiply", "multiplies two numbers, payload = 'a,b'"]])
else
if role == "clock" then
signal_announce(task_name, port_id, [["time", "reports the current server time"]])
end
end
end
set_var("should_stop", "0")
while get("__vars", "should_stop") == "0" do
signal_poll(port_id, 300)
end
end
The orchestrator (self_hosting/examples/microservices_demo.patlang)
include "../lib/signal_discovery.patlang"
make a function called wait_for_ready takes task_name, pid, timeout_ms returns result
# polls is_alive(pid) AND signal_discover_one(task_name, ...) together --
# neither alone can distinguish "still starting up" from "already died"
let waited = 0
let looping = true
let outcome = ["timeout", "", []]
while looping do
if not is_alive(pid) then
let outcome = ["crashed", "", []]
let looping = false
else
let found = signal_discover_one(task_name, 60000)
if found[0] != "" then
let outcome = ["ready", found[0], found[1]]
let looping = false
else
if waited >= timeout_ms then
let looping = false
else
sleep_ms(100)
let waited = waited + 100
end
end
end
end
return outcome
end
make a function called spawn_service takes role returns pid_and_result
let pid = spawn(pat_path(), "--ir-run", worker_path(), role)
let result = wait_for_ready("svc_" + role, pid, 5000)
return [pid, result]
end
make a function called dispatch takes keyword, payload, description returns done
let candidates = signal_find_capable(keyword, 60000)
if to_num(list_len(candidates)) == 0 then
log_line(description + " -- no service currently offers '" + keyword + "'")
return true
end
let match = candidates[0]
signal_wrap(match[0], match[1], [[match[2], ""]])
let reply = signal_call(match[0], match[2], payload)
log_line(" -> reply: " + reply)
return true
end
let greeter = spawn_service("greeter")
let calculator = spawn_service("calculator")
let clock = spawn_service("clock")
dispatch("greet", "Pat", "asked who can 'greet'")
dispatch("add", "17,25", "asked who can 'add'")
dispatch("multiply", "6,7", "asked who can 'multiply'")
dispatch("time", "", "asked who can report 'time'")
dispatch("translate", "hello", "asked who can 'translate'") # an honest miss
signal_send(to_num(greeter[1][1]), "quit", "")
signal_send(to_num(calculator[1][1]), "quit", "")
signal_send(to_num(clock[1][1]), "quit", "")
wait(greeter[0])
wait(calculator[0])
wait(clock[0])
(Trimmed for readability above — the real source has the full logging around every step. See the repository for the complete files.)
A real, captured transcript
Unedited output from actually running the orchestrator, timestamps shown as milliseconds elapsed since the run started rather than raw epoch numbers. Confirmed identical, byte for byte in content, whether run interpreted (pat --ir-run) or compiled natively via the self-hosted compiler (patc1.exe) — and confirmed clean across multiple repeated runs, not just one lucky pass.
[+0ms] orchestrator: starting -- spawning 3 independent microservices
[+7ms] spawned 'greeter' service (pid 63352)
[+108ms] 'greeter' is ready on port 62353 -- offers: greet
[+113ms] spawned 'calculator' service (pid 48556)
[+215ms] 'calculator' is ready on port 62354 -- offers: add, multiply
[+221ms] spawned 'clock' service (pid 61152)
[+323ms] 'clock' is ready on port 62355 -- offers: time
[+323ms] orchestrator: dispatching real requests, purely by discovered capability
[+324ms] asked who can 'greet' -- found 'svc_greeter' offers 'greet', calling it
[+330ms] -> reply: Hello, Pat! (from the greeter microservice)
[+331ms] asked who can 'add' -- found 'svc_calculator' offers 'add', calling it
[+334ms] -> reply: 42
[+335ms] asked who can 'multiply' -- found 'svc_calculator' offers 'multiply', calling it
[+336ms] -> reply: 42
[+337ms] asked who can report 'time' -- found 'svc_clock' offers 'time', calling it
[+338ms] -> reply: current server time: 1784311111704ms since epoch
[+338ms] orchestrator: demonstrating an honest miss -- nobody offers this
[+340ms] asked who can 'translate' -- no service currently offers 'translate'
[+340ms] orchestrator: shutting down all services
[+351ms] orchestrator: done -- all services exited cleanly
Worth a second look: add(17, 25) and multiply(6, 7) both land on 42 in this particular run — a genuine coincidence of the example numbers chosen (17+25=42, 6×7=42 both really are correct), not a routing bug where multiply silently called add. Checked directly with different numbers before trusting the transcript: add(10, 3) gives 13, multiply(10, 3) gives 30 — confirming each request really did reach the calculator's own distinct handler for that specific action, not just "some handler that happened to return the right-looking number once."
What this surfaced
One real, previously-unknown bug came directly out of building this, not from hunting for one: the worker's when handlers were first written conditionally, gated on which role the process was playing (if role == "greeter" then when greet do ... end end), which read naturally enough given every other conditional in the file. Every request came back completely empty. A minimal repro settled it in one line: if true then when x do print("fired") end end followed by emit("x", "") prints nothing at all — a when handler registered inside a conditional block simply never fires, silently, no error anywhere. The fix was to register every handler unconditionally at true top level instead, relying on the fact that a given process only ever actually gets asked for the one action its own role really offers — the other handlers exist in memory, harmlessly unused. Worth knowing for any future PatLang program: when is a top-level declaration, not a conditionally-executable statement, and nothing currently warns you if you write it the other way.
See also
Presence, Discovery, and Non-Blocking Spawn covers every piece of infrastructure this demo combines, in isolation, with its own worked examples and the design decisions behind each one. Runtime signals and the message queue are the two original building blocks underneath all of it.