Tutorial · 2026-08-22 · Three stops on one road: a bare function served as-is, a declared service with a table and a contract, and finally a journal — the kind of sales record a law can rely on. Every listing runs; total time, about twenty minutes.
Start where every idea starts — a function in a file:
# till.ring func total qty, price return qty * price
$ ringserv serve till.ring $ curl -X POST localhost:8080/api/v1 \ -d '{"service":"till","action":"total", "payload":{"qty":2,"price":180}}' {"code":0,"message":"OK","data":360}
No declarations, no routes. The payload's keys were matched to the
parameters by name. When you wonder what exactly is exposed,
ringserv serve --explain till.ring prints the whole mapping and exits.
A real till needs to remember. Graduate the file — same binary, same call shape, more said:
RingServ([ :port = 8080, :database = "till.db", :data = [ :sales = [ :item = :text, :amount = :number ] ], :services = [ :sales = [ :table = "sales" ] # list/add/edit/delete, one line ] ]) Contract(:sales, [ :create = [ :in = [ :item = [ :type = :string, :required = true ], :amount = [ :type = :number, :min = 0 ] ] ] ])
Two things happened. The table became a service — five actions from one
line, with SQLite (in a file you can copy for backup) underneath. And the contract put
validation at the door: a request missing item is refused with every
problem named at once, before your code runs. An action that checks its own payload is
an action written twice.
Now the part most stacks make you build yourself, badly, under deadline. Sales are money, and in several countries the law says a sales record must be inalterable. A table cannot promise that — anyone with the file can edit a row. A journal can:
Journal([ :name = "sales", :apply = func aEvent { # fold each event into today's totals return 1 } ]) # inside your action: JournalAppend("sales", [ :type = "sold", :item = "espresso", :amount = 360 ])
Three properties, each one testable. Append-only: the journal's
own service exposes read and verify — an "edit" action does not exist to be called.
Sealed: every entry carries a fingerprint of the one before it, so if anyone
edits the file with a database tool, ringserv journal verify answers
broken, at entry N, and why — and exits with a failure code, so a nightly cron
job tells you instead of an auditor. Replayed: your running totals live in
memory, rebuilt from the journal at startup — kill the server mid-sale and nothing is
lost, because there is nothing outside the record to lose.
And when a customer cancels? You append a cancelled event
with a reason. The sale stays in history with its cancellation beside it — which is
exactly the difference between a record and a table.
The full tutorial arc continues: a JavaScript service, offline sync, and placement. And the reference application is this article grown all the way up — a café counter using every form at once, with 38 tests driving it.