Learn ARO

A journey from zero to building event-driven applications with natural language-like code.

Estimated read time: 15 minutes

Begin the journey
01

Once Upon a Time...

Why ARO exists

Imagine you're explaining your software to a business colleague. You wouldn't say "We instantiate a UserService singleton and invoke the createUser method with a DTO". You'd say "We create a user with their registration data".

What if code could read like that conversation? What if the gap between business requirements and implementation simply... disappeared?

ARO (Action-Result-Object) was born from a simple idea: code should express intent, not implementation details. It's a language designed for business applications where clarity matters more than cleverness.

In ARO, every statement follows a natural pattern that anyone can understand:

aro
<Create> the <user> with <registration-data>.

That's it. No boilerplate. No ceremony. Just clear, declarative intent.

02

The Building Block

Understanding ARO statements

Every line of ARO code follows the same elegant pattern:

 The ARO Statement Pattern
 -------------------------

 <Action>  the  <result>  preposition  the  <object>.
    |            |             |              |
    |            |             |              |
 What to do   Output      Relationship     Input


 Examples:

 <Extract> the <email>    from    the <request: body>.
 <Create>  the <user>     with    the <user-data>.
 <Store>   the <user>     into    the <repository>.
 <Return>  an  <OK>       for     the <request>.

Actions are verbs that describe what you want to do. They're categorized by their role in data flow:

REQUEST Actions

Bring data in: <Extract>, <Retrieve>, <Fetch>

OWN Actions

Transform data: <Create>, <Compute>, <Validate>

EXPORT Actions

Send data out: <Store>, <Log>, <Send>

RESPONSE Actions

Complete the flow: <Return>, <Throw>

Prepositions give meaning to relationships: from for sources, with for data, into for destinations, for for purpose.

03

Feature Sets

Organizing business logic

Statements don't live alone — they're grouped into feature sets. A feature set is a named block of code that accomplishes a specific business goal.

aro
(* A feature set has a name and a business activity *)

(Create User Account: User Management) {
    (* Extract registration data from the request *)
    <Extract> the <email> from the <request: body email>.
    <Extract> the <password> from the <request: body password>.

    (* Validate the data *)
    <Validate> the <email> for the <email-format>.

    (* Create and store the user *)
    <Create> the <user> with { email: <email>, password: <password> }.
    <Store> the <user> into the <user-repository>.

    (* Return success *)
    <Return> a <Created: status> with <user>.
}
Notice how this reads almost like a checklist? Extract the data, validate it, create the user, store them, return success. Anyone can understand what this does.

The feature set name (Create User Account) describes the action. The business activity (User Management) provides context and grouping.

Feature sets are never called directly. Instead, they respond to events — HTTP requests, file changes, socket connections, or custom domain events. This is a fundamental principle of ARO.

04

The Application Lifecycle

How ARO applications live and breathe

Every ARO application needs exactly one entry point: Application-Start. This is where your application begins its life.

1. Startup

Application-Start executes. Initialize services, load configuration, prepare for events.

2. Keepalive

For long-running apps (servers, watchers), <Keepalive> keeps the app alive to process events.

3. Event Loop

The application listens for events: HTTP requests, file changes, socket data. Each event triggers matching feature sets.

4. Shutdown

On SIGTERM/SIGINT, Application-End: Success runs for graceful cleanup.

aro
(* The required entry point *)
(Application-Start: My API Server) {
    <Log> the <message> for the <console> with "Starting server...".

    (* Keep the app running to handle HTTP requests *)
    <Keepalive> the <application> for the <events>.

    <Return> an <OK: status> for the <startup>.
}

(* Optional: cleanup on shutdown *)
(Application-End: Success) {
    <Log> the <message> for the <console> with "Goodbye!".
    <Return> an <OK: status> for the <shutdown>.
}
05

Contract-First HTTP

Your API starts with a contract

ARO uses contract-first development. You define your API in an openapi.yaml file, and ARO routes requests to feature sets based on the operationId.

    openapi.yaml                      handlers.aro
    ------------                      ------------

    paths:                            (listUsers: User API) {
      /users:                             <Retrieve> the <users>...
        get:              -------->       <Return> an <OK>...
          operationId: listUsers      }
        post:
          operationId: createUser  -> (createUser: User API) {
      /users/{id}:                        <Extract> the <data>...
        get:                              <Create> the <user>...
          operationId: getUser     ->     <Return> a <Created>...
                                      }

                                      (getUser: User API) {
                                          <Extract> the <id>...
                                          <Retrieve> the <user>...
                                          <Return> an <OK>...
                                      }

The feature set name must match the operationId. No magic annotations, no decorators — just naming conventions.

yaml
# openapi.yaml - Your API contract
openapi: 3.0.3
info:
  title: User API
  version: 1.0.0

paths:
  /users:
    get:
      operationId: listUsers  # Maps to feature set name
    post:
      operationId: createUser
  /users/{id}:
    get:
      operationId: getUser
      parameters:
        - name: id
          in: path
          required: true
aro
(* Feature set names match operationIds *)

(listUsers: User API) {
    <Retrieve> the <users> from the <user-repository>.
    <Return> an <OK: status> with <users>.
}

(getUser: User API) {
    <Extract> the <id> from the <pathParameters: id>.
    <Retrieve> the <user> from the <user-repository> where id = <id>.
    <Return> an <OK: status> with <user>.
}

(createUser: User API) {
    <Extract> the <data> from the <request: body>.
    <Create> the <user> with <data>.
    <Store> the <user> into the <user-repository>.
    <Return> a <Created: status> with <user>.
}
06

Event-Driven Architecture

React to the world around you

ARO is fundamentally event-driven. Feature sets don't call each other directly — they respond to events. This creates loose coupling and natural extensibility.

HTTP Events

Requests route via operationId from your OpenAPI contract.

File Events

FileCreated, FileModified, FileDeleted from watched directories.

Socket Events

ClientConnected, DataReceived, ClientDisconnected for TCP.

Event handlers use "Handler" in their business activity name:

aro
(* File system event handler *)
(Process Upload: FileCreated Handler) {
    <Extract> the <path> from the <event: path>.
    <Read> the <content> from the <file: path>.
    <Log> the <message> for the <console> with "Processing: ${path}".
    <Return> an <OK: status> for the <processing>.
}

(* Socket event handler - echo server *)
(Echo Data: DataReceived Handler) {
    <Extract> the <data> from the <event: data>.
    <Extract> the <client> from the <event: connection>.
    <Send> the <data> to the <client>.
    <Return> an <OK: status> for the <echo>.
}
Think of feature sets as workers waiting for work. When an event arrives, all matching handlers spring into action — independently and concurrently.
07

The ARO Philosophy

Design principles that guide the language

I

Clarity Over Cleverness

Code should be readable by anyone, not just the author. Natural language patterns make intent obvious.

II

Contract-First

APIs are defined before implementation. The contract is the source of truth, not an afterthought.

III

Events, Not Calls

Feature sets respond to events, never calling each other directly. This creates natural decoupling.

ARO isn't trying to be the fastest or most powerful language. It's trying to be the most understandable. Because code that everyone can read is code that everyone can improve.

ARO focuses on the "happy path". You write the code for when things work; the runtime handles errors gracefully. This isn't production-ready error handling — it's intentional simplicity for exploration and prototyping.

aro
(* ARO handles the unhappy path automatically *)
(getUser: User API) {
    <Extract> the <id> from the <pathParameters: id>.
    <Retrieve> the <user> from the <user-repository> where id = <id>.
    <Return> an <OK: status> with <user>.
}

(* If user not found, runtime returns:
   "Cannot retrieve the user from the user-repository where id = 123"

   No try/catch, no null checks, no boilerplate. *)
08

Putting It All Together

A complete ARO application

Here's a complete application structure. Notice how everything connects:

MyAPI/
├── openapi.yaml      <-- Defines HTTP routes
├── main.aro          <-- Application lifecycle
└── handlers.aro      <-- Feature sets for each operationId
yaml
# openapi.yaml
openapi: 3.0.3
info:
  title: Greeting API
  version: 1.0.0
paths:
  /hello:
    get:
      operationId: sayHello
      parameters:
        - name: name
          in: query
          schema:
            type: string
aro
(* main.aro *)
(Application-Start: Greeting API) {
    <Log> the <message> for the <console> with "Greeting API ready!".
    <Keepalive> the <application> for the <events>.
    <Return> an <OK: status> for the <startup>.
}

(* handlers.aro *)
(sayHello: Greeting API) {
    <Extract> the <name> from the <queryParameters: name>.

    if <name> is empty then {
        <Create> the <name> with "World".
    }

    <Create> the <greeting> with { message: "Hello, ${name}!" }.
    <Return> an <OK: status> with <greeting>.
}
bash
# Run the application
$ aro run ./MyAPI

Greeting API ready!
Server running on http://localhost:8080

# Test it
$ curl http://localhost:8080/hello?name=Developer
{"message": "Hello, Developer!"}
09

Context-Aware Output

Same code, different formats

ARO automatically formats output based on how your code runs. The same <Log> and <Return> statements produce different output depending on the execution context.

Human Context

aro run — Clean, readable text for humans at the terminal.

Machine Context

HTTP APIs — JSON output for programmatic consumption.

Consider this simple feature set:

aro
(getUser: User API) {
    <Retrieve> the <user> from the <user-repository> where id = <id>.
    <Log> the <status> for the <console> with "User retrieved".
    <Return> an <OK: status> with <user>.
}

When run from the CLI with aro run, humans see this:

Human Context
[getUser] User retrieved
[OK] success
  user.id: 123
  user.name: Alice

When accessed via HTTP API, machines receive JSON:

Machine Context (JSON)
{
  "status": "OK",
  "reason": "success",
  "data": {
    "user": {"id": 123, "name": "Alice"}
  }
}
Write once, run anywhere. Your ARO code adapts its output to serve humans at the terminal, developers during testing, and machines via APIs — all without a single format string.

Your Journey Begins

Ready to write some ARO?

You've learned the fundamentals. Now it's time to explore deeper. Build something. Break something. Learn by doing.