01
Greeting, on a Box
How hard is it to say Hello World? It shouldn't be much trouble. Let’s try with Go.
package main
import "fmt"
func sayHello(toWhom string) string {
return "Hello " + toWhom
}
func main() {
fmt.Println(sayHello("World"))
}
That was not too hard – about five lines of code to the eye.
02
Remember a User
How about remembering whom we've already greeted? That's not so easy but not too tough either:
package main
import "fmt"
var users = map[string]bool{}
func sayHello(toWhom string) string {
if _, ok := users[toWhom]; ok {
return "Hello registered user " + toWhom
}
users[toWhom] = true
return "Hello new user " + toWhom
}
func main() {
fmt.Println(sayHello("World"))
fmt.Println(sayHello("World"))
}
That looks a bit more involved but still not too difficult to follow, even if you don't know Go. It is, however, still a single process with temporary memory.
03
Cross the Machine Boundary
Let's turn the greeter into a minimal distributed application. We need:
- 01Persistent registry. A restart must not forget whom we have greeted.
- 02Network API. Other processes must be able to ask for a greeting.
- 03Concurrent requests. Registration must remain correct while calls overlap.
package main
import (
"bufio"
"fmt"
"log"
"net/http"
"os"
"sync"
)
const userFile = "users.txt"
type Registry struct {
mu sync.Mutex
users map[string]bool
}
func load(path string) map[string]bool {
users := make(map[string]bool)
f, err := os.Open(path)
if err != nil {
return users
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
users[scanner.Text()] = true
}
return users
}
func (r *Registry) greet(name string) string {
r.mu.Lock()
defer r.mu.Unlock()
if r.users[name] {
return "Hello registered user " + name
}
r.users[name] = true
flags := os.O_APPEND |
os.O_CREATE |
os.O_WRONLY
f, err := os.OpenFile(
userFile,
flags,
0644,
)
if err != nil {
return "Hello new user " + name +
" (not saved)"
}
defer f.Close()
fmt.Fprintln(f, name)
return "Hello new user " + name
}
func hello(r *Registry) http.HandlerFunc {
return func(
w http.ResponseWriter,
req *http.Request,
) {
name := req.URL.Query().Get("name")
if name == "" {
http.Error(
w,
"missing name",
http.StatusBadRequest,
)
return
}
fmt.Fprintln(w, r.greet(name))
}
}
func main() {
registry := &Registry{
users: load(userFile),
}
http.HandleFunc("/hello", hello(registry))
log.Fatal(http.ListenAndServe(":8080", nil))
}
You know what, don’t bother reading the code above. It’s okay. While I wrote the first two Go snippets by hand, I got an LLM to write this one and I didn’t bother reading it myself!
04
The Problem
The program ceases to be correct when run as multiple instances. Each instance has its own in-memory registry and all of them append to a file that was never designed to be shared this way.
A real version now needs a storage service, a schema and client, connection management, consistency model, transaction rules, service discovery, authentication, deployment configuration, health checks, scaling policy, observability and a failure model.
The domain operation remains “remember a user and greet them.” The implementation has become mostly machinery required to make that operation survive distribution. You could try to ease the pain using a framework, but there's still a problem. A framework is open. It can only assume that things are a certain way. As long as you're programming in a lower model, using a framework just means that you promise to follow the instructions on the label. It's not too hard to inadvertently go against the instructions. Therein lie production outages.
Writing "Hello World" on a computer involves processors, memory, storage, device drivers, operating systems, compilers and runtimes, yet we do it without skipping a beat because the programming model is there. Distributed applications do not yet have such a model, so every developer must reason directly about networks, parallel execution, partial failure, retries, data ownership and operations. The good news is that we now know just enough to build one.
05
The Ritchie Program
RITE — Reliable Interaction with Typed Endpoints — distils what we have collectively learned into a small model built around single ownership, bounded calls, idempotent retries and invariants that do not cross boundaries. Ritchie is the language that enforces the programming model
Ritchie makes durable identity and the network boundary visible in the program. The entity owns persistence and transactional state. The service publishes the operation.
$User {
unique String name
}
$User.name() -> String {
return self.name
}
@Greeter { }
@Greeter.sayHello(String toWhom) -> String {
$User u = $User[name: toWhom]
else create $User{} then {
return f"Hello new user {toWhom}"
}
return f"Hello registered user {u.name()}"
}
| Source form | Meaning |
|---|---|
$User | Persistent state with durable identity. |
$User.name() | Behaviour that owns access to entity state. |
else create | Atomic get-or-create. |
@Greeter | A stateless networked application boundary. |
@Greeter.sayHello() | An API endpoint of the Greeter service. |
The source retains the domain operation while giving the compiler and runtime the structure they need to provide persistence, concurrency and distributed execution.
06
Build and deploy
With the current Ritchie toolchain installed, check the source and compile it into an immutable, environment-neutral release:
rsc check hello.rsc
rsc build \
--application ritchie.dev.hello \
--output hello.rap \
hello.rsc
Create an environment on a configured runtime site and deploy the same release
rap environment create hello-dev --site nswcloud
rap deploy --environment hello-dev hello.rap
The compiler has a program graph containing a service, an entity, a unique index, call edges and boundary signatures. This graph gets translated into a RITE-compliant deployment topology comprising workloads, persistence, routing, policy and recovery for the site environment.
07
A Bit More...
The previous Ritchie program demonstrated the functionality that we had in the second version of Hello World, but distributed. Let's extend it a bit more. We want:
- 01Overlapping Names. Because, of course, more than one person can have the same name.
- 02Unique Emails. That's how we know who's actually who.
- 03Stats! Let's find the most popular names.
Here's the Ritchie Program for that:
Email = String
$User {
String name
unique Email email
}
$User.name() -> String {
return self.name
}
$User.email() -> Email {
return self.email
}
%UStats {
String name
Int total
}
%UStats.stats() {
from $User u
group by u.name
select { name: u.name, total: count(u) }
sort total desc
}
@Greeter {}
@Greeter.sayHello(String toWhom, Email email) -> String {
$User u = $User[email: email]
else create $User { name:toWhom } then {
return f"Hello new user {toWhom} <{email}>"
}
return f"Hello registered user {u.name()} <{u.email()}>"
}
@Greeter.stats() -> String {
String[] stats = []
for row in %UStats.stats() {
stats = append(stats, f"{row.name}: {row.total}")
}
return stats.join(", ")
}
@Gateway.external(endpoint: @Greeter.sayHello, budget: 1500ms)
@Gateway.external(endpoint: @Greeter.stats, budget: 1000ms)
This program introduces a few more interesting Ritchie concepts.
| Source form | Meaning |
|---|---|
Email = String | A type alias. Use := for a new type. |
%UStats | A Projection, used for eventually consistent reads. |
%UStats.stats() | A Projection query, used to filter, sort or aggregate entities. |
@Gateway | A built-in service that exposes selected Ritchie service methods through HTTPS/JSON. |
@Gateway.external() | Registers specific service endpoints for external access. |
When deployed, rap created a Kubernetes development environment for this program. It provisioned:
- A persistent
$Userentity service backed by PostgreSQL, including its schema and unique email constraint. - A replicated
@Greeterservice that calls$Userand the query collection. - A
%UStatscollection service that executes the grouped, sorted read query with read-only database access. - An HTTPS Gateway exposing
sayHelloandstatsthrough NodePort32031, with their declared request budgets. - Internal headless Services, DNS bindings, mutual-TLS identities, authorization rules, NetworkPolicies, runtime budgets, and readiness checks connecting those units.
- A shared PostgreSQL database with Ritchie-managed schemas and roles.
- Imported OCI images for the entry point, entity, service, collection, Gateway, and schema-management artifact.
Here's a sample run (simplified presentation) of the program:
$ URL='https://127.0.0.1:32031/ritchie.dev.hello/v1/main/Greeter'
$ OPTS=(-ksS -H 'Content-Type: application/json')
$ curl "${OPTS[@]}" -d '{"toWhom":"World","email":"helloworld@ritchie.dev"}' "$URL/sayHello" | jq -r .
Hello new user World <helloworld@ritchie.dev>
$ curl "${OPTS[@]}" -d '{"toWhom":"World","email":"helloworld@ritchie.dev"}' "$URL/sayHello" | jq -r .
Hello registered user World <helloworld@ritchie.dev>
$ curl "${OPTS[@]}" -d '{"toWhom":"World","email":"helloworldagain@ritchie.dev"}' "$URL/sayHello" | jq -r .
Hello new user World <helloworldagain@ritchie.dev>
$ curl "${OPTS[@]}" -d '{"toWhom":"Ada","email":"ada@example.com"}' "$URL/sayHello" | jq -r .
Hello new user Ada <ada@example.com>
$ curl "${OPTS[@]}" -d '{}' "$URL/stats" | jq -r .
World: 2, Ada: 1
This preview shows the power of Ritchie as a distributed application programming language. The reason why there is little else beyond a preview at the moment is that the developer experience is still not polished and there is a lot of "bulking up" of the language facilities, especially a standard library, that needs to be done.
