Hewing Your First Distributed Service

In six steps you'll go from Hello World to a distributed key-value store — learning actors, supervision, and wire types along the way.

Step 1: Hello World

Every Hew program starts with a main function. Let's begin with the basics: bindings, types, and functions.

hello.hew
fn main() -> i32 {    // Immutable binding    let greeting: String = "Hello, Hew!";    println_str(greeting);    // Mutable binding    var count: i32 = 0;    count = count + 1;    // Other primitive types    let pi: f64 = 3.14159;    let active: bool = true;    0  // exit code}

let creates an immutable binding; var creates a mutable one. Hew's core types include i32, f64, String, and bool.

Functions can take parameters and return values:

functions.hew
fn add(a: i32, b: i32) -> i32 {    a + b}fn greet(name: String) -> String {    "Hello, " + name + "!"}fn main() -> i32 {    let sum = add(3, 4);    println_int(sum);          // 7    let msg = greet("world");    println_str(msg);          // Hello, world!    0}

Compile and run with hewc hello.hew && ./hello. Now let's move on to the feature that makes Hew special: actors.

Step 2: Actors & Messages

An actor is an isolated, single-threaded state machine with a mailbox. Actors never share mutable state — they communicate exclusively through messages. This gives you compile-time data-race freedom without locks.

Define an actor with the actor keyword. Message handlers are marked with receive fn; internal helpers use plain fn. Callers invoke handlers as direct method calls on the actor reference — no .send() or .ask() needed.

counter.hew
actor Counter {    var count: i32 = 0;    // Fire-and-forget: no return type, caller does not await    receive fn increment() {        self.count = self.count + 1;    }    // Fire-and-forget with a parameter    receive fn add(n: i32) {        self.count = self.count + n;    }    // Request-response: has return type, caller must await    receive fn get_count() -> i32 {        self.count    }}fn main() -> i32 {    // Spawn an actor    let counter = spawn Counter { count: 0 };    // Fire-and-forget — direct method calls, no await needed    counter.increment();    counter.increment();    counter.add(10);    0}

spawn creates a new actor and returns an ActorRef. Fire-and-forget calls like counter.increment() enqueue a message and return immediately. Each actor processes one message at a time, so there are never data races on count.

Step 3: Request-Response Patterns

Fire-and-forget is great for commands, but sometimes you need a reply. A receive fn with a return type creates a request-response handler. Callers await the method call to get the result — no .ask() needed.

calculator.hew
actor Calculator {    var history: Vec<String> = Vec::new();    receive fn compute(op: String, a: f64, b: f64) -> Result<f64, String> {        let result = if op == "add" {            Ok(a + b)        } else if op == "mul" {            Ok(a * b)        } else if op == "div" {            if b == 0.0 {                Err("division by zero")            } else {                Ok(a / b)            }        } else {            Err("unknown operation")        };        // Record in history        if let Ok(val) = result {            self.history.push(op + ": " + f64_to_string(val));        }        result    }    receive fn get_history() -> Vec<String> {        self.history.clone()    }}fn main() -> i32 {    let calc = spawn Calculator { history: Vec::new() };    // Direct method call; await blocks until the reply arrives    let sum = await calc.compute("add", 10.0, 20.0);    match sum {        Ok(val) => println_str("Sum: " + f64_to_string(val)),        Err(e) => println_str("Error: " + e),    }    // The ? operator propagates errors    let product = await calc.compute("mul", 3.0, 7.0)?;    println_str("Product: " + f64_to_string(product));    0}

Result<T, E> is Hew's standard error type. The ? operator propagates errors — if the result is Err, the function returns early. Combined with await and direct method calls, you get synchronous-style code with full concurrency under the hood.

Step 4: Supervision & Fault Tolerance

In production services, crashes are inevitable. Hew follows Erlang's "let it crash" philosophy: instead of defensive error handling everywhere, you define supervisors that automatically restart failed actors.

supervised_pool.hew
actor Worker {    var id: i32 = 0;    receive fn process(job: String) {        println_str("Worker " + int_to_string(self.id) + " processing: " + job);        // If this traps, the supervisor restarts us    }}// Supervisor owns and monitors child actorssupervisor WorkerPool {    strategy: one_for_one,    max_restarts: 5,    window: 60s,    children {        child w1: Worker restart(permanent);        child w2: Worker restart(permanent);        child w3: Worker restart(transient);    }}

Key concepts:

  • one_for_one: only the crashed child is restarted — siblings keep running.
  • permanent: always restart, regardless of exit reason.
  • transient: restart only on abnormal exit (crashes), not on normal shutdown.
  • temporary: never restart — once stopped, it's gone.
  • max_restarts / window: if the supervisor exceeds 5 restarts in 60 seconds, the failure escalates to the parent supervisor.

Supervisors form a tree. Your top-level supervisor owns mid-level supervisors, which own workers. A crash in a leaf actor is contained and recovered automatically — your service stays up.

Step 5: Wire Types & Networking

Actors on the same machine communicate via in-process messages. But distributed services need to talk over the network. Hew's wire struct and wire enum give you network-ready serialization with built-in schema evolution.

protocol.hew
// Every field has a numeric tag (@1, @2, ...) for stable serialization.// Tags must NEVER be reused — even after deleting a field.wire struct PutRequest {    key: string @1;    value: string @2;    ttl: i32? @3;     // optional field}wire struct GetRequest {    key: string @1;}wire struct GetResponse {    found: bool @1;    value: string? @2;}wire enum RequestType {    Get;    Put;    Delete;}

Schema evolution rules:

  • Never reuse a tag number — deleted fields should have their tags reserved.
  • New fields must be optional (?) or have defaults so old readers can still decode.
  • Encoding is automatic — Hew generates efficient binary serialization (HBF) from wire definitions.
  • Use hew wire check --against <schema> in CI to catch breaking changes.

Wire types implement the Send trait, so they can be used directly as actor messages — both locally and across the network.

Step 6: Building a Distributed Key-Value Store

Let's tie everything together. We'll build a key-value store with a frontend actor that accepts client requests and a backend actor that stores data.

Message protocol

kv_protocol.hew
// Wire types for network transportwire struct PutMsg {    key: string @1;    value: string @2;}wire struct GetMsg {    key: string @1;}wire struct GetResult {    found: bool @1;    value: string? @2;}

Backend actor

kv_backend.hew
actor KVBackend {    var store: HashMap&lt;String, String&gt; = HashMap::new();    receive fn put(msg: PutMsg) {        self.store.insert(msg.key, msg.value);    }    receive fn get(msg: GetMsg) -> GetResult {        match self.store.get(msg.key) {            Some(val) => GetResult { found: true, value: Some(val.clone()) },            None => GetResult { found: false, value: None },        }    }    receive fn count() -> i32 {        self.store.len()    }}

Frontend actor

kv_frontend.hew
actor KVFrontend {    var backend: ActorRef&lt;KVBackend&gt;;    receive fn handle_put(msg: PutMsg) {        // Fire-and-forget to backend        self.backend.put(msg);    }    receive fn handle_get(msg: GetMsg) -> GetResult {        // Request-response to backend        let result = await self.backend.get(msg);        result    }}

Supervision tree

kv_supervisor.hew
supervisor KVSupervisor {    strategy: one_for_one,    max_restarts: 10,    window: 60s,    children {        child backend: KVBackend restart(permanent);        child frontend: KVFrontend restart(permanent);    }}

Putting it all together

main.hew
fn main() -> i32 {    // Start the supervised service    let sup = spawn KVSupervisor;    // Get references to child actors    let frontend = sup.child_ref("frontend");    // Store some data — direct method calls, fire-and-forget    frontend.handle_put(PutMsg { key: "lang", value: "hew" });    frontend.handle_put(PutMsg { key: "version", value: "0.5" });    // Retrieve data — direct method call, await for response    let result = await frontend.handle_get(GetMsg { key: "lang" });    match result.value {        Some(val) => println_str("Found: " + val),        None => println_str("Not found"),    }    // Use select for timeout on slow operations    let guarded = select {        value from frontend.handle_get(GetMsg { key: "version" }) => value,        after 2.seconds => GetResult { found: false, value: None },    };    0}

You now have a complete distributed service: wire types define the protocol, actors handle concurrency without locks, and a supervisor keeps everything running through failures. Named actors use direct method calls; lambda actors use the <- operator. The select expression lets you race concurrent operations or apply timeouts. From here, you can add more backends for sharding, plug in network listeners for remote clients, or grow the supervision tree as your service scales.

Next steps

  • Actor Model — deep dive into message passing, lambda actors, and structured concurrency
  • Type System — generics, capabilities, and pattern matching
  • Standard Library — built-in types and IO