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() {
    let greeting = "Hello, Hew!";
    println(greeting);
    var count = 0;
    count = count + 1;
    let pi: f64 = 3.14159;
    let active: bool = true;
    println(f"count={count}, pi={pi}");
}

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

Functions can take parameters and return values:

functions.hew
fn add(a: Int, b: Int) -> Int {
    a + b
}
fn greet(name: String) -> String {
    f"Hello, {name}!"
}
fn main() {
    let sum = add(3, 4);
    println(sum);
    let msg = greet("world");
    println(msg);
}

Compile and run with hew build hello.hew -o hello && ./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 {
    let count: Int;
    // Fire-and-forget: no return type, caller does not await
    receive fn increment() {
        count = count + 1;
    }
    // Fire-and-forget with a parameter
    receive fn add(n: Int) {
        count = count + n;
    }
    // Request-response: has return type, caller must await
    receive fn get_count() -> Int {
        count
    }
}
fn main() {
    let counter = spawn Counter;
    // Fire-and-forget — direct method calls, no await needed
    counter.increment();
    counter.increment();
    counter.add(10);
    // Request-response — await the reply
    let n = await counter.get_count();
    println(f"count = {n}");
}

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 {
    let ops: Int;
    receive fn compute(op: String, a: Int, b: Int) -> Int {
        ops = ops + 1;
        if op == "add" { a + b }
        else if op == "mul" { a * b }
        else if op == "div" { a / b }
        else { 0 }
    }
}
fn main() {
    let calc = spawn Calculator;
    let sum = await calc.compute("add", 10, 20);
    println(f"Sum: {sum}");
    let product = await calc.compute("mul", 3, 7);
    println(f"Product: {product}");
}

The caller awaits the method call to get the result. Under the hood, the message is sent to the actor, processed, and the reply is returned — all with direct method-call syntax. For error handling, Hew provides Result<T, E> with the ? operator for propagation.

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 {
    let id: Int;
    receive fn process(job: string) {
        println(f"Worker {id} processing: {job}");
    }
}
supervisor WorkerPool {
    child w1: Worker
        restart(permanent)
        budget(5, 60.seconds)
        strategy(one_for_one);
    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.
  • budget(n, period): if the supervisor exceeds n restarts in period, 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 type and wire enum give you network-ready serialization with built-in schema evolution.

protocol.hew
wire type PutRequest {
    key: String @1;
    value: String @2;
    ttl: i32 @3 optional;
}
wire type GetRequest {
    key: String @1;
}
wire type GetResponse {
    found: bool @1;
    value: String @2 optional;
}
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 (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 type PutMsg {
    key: String @1;
    value: String @2;
}
wire type GetMsg {
    key: String @1;
}
type GetResult {
    found: bool;
    value: String;
}

Backend actor

kv_backend.hew
actor KVBackend {
    let store: HashMap<String, String>;
    receive fn put(key: String, value: String) {
        store.insert(key, value);
    }
    receive fn get(key: String) -> GetResult {
        match store.get(key) {
            Some(val) => GetResult { found: true, value: val },
            None => GetResult { found: false, value: "" },
        }
    }
}

Frontend actor

kv_frontend.hew
actor KVFrontend {
    let backend: KVBackend;
    receive fn handle_put(key: String, value: String) {
        backend.put(key, value);
    }
    receive fn handle_get(key: String) -> GetResult {
        await backend.get(key)
    }
}

Supervision tree

kv_supervisor.hew
supervisor KVSupervisor {
    child backend: KVBackend
        restart(permanent)
        budget(10, 60.seconds)
        strategy(one_for_one);
    child frontend: KVFrontend
        restart(permanent);
}

Putting it all together

main.hew
fn main() {
    let backend = spawn KVBackend;
    let frontend = spawn KVFrontend(backend: backend);
    // Store some data — fire-and-forget
    frontend.handle_put("lang", "hew");
    frontend.handle_put("version", "0.5");
    // Retrieve data — await for response
    sleep_ms(100);
    let result = await frontend.handle_get("lang");
    if result.found {
        println(f"Found: {result.value}");
    } else {
        println("Not found");
    }
}

You now have a complete service: wire types define the protocol, actors handle concurrency without locks, and supervisors keep everything running through failures. Named actors use direct method calls; lambda actors use the <- send operator. 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