How It Works

Knowing the architecture helps you write better code and -- more importantly -- debug problems without losing your mind. So let me walk through the key mechanisms that make Adama tick.

The Document-Centric Model

In Adama, the document is the fundamental unit of everything. All state lives in a document. All code runs in a document. All privacy is enforced by a document. All connections are handled by a document. All changes are persisted by a document. The document is the universe.

// This entire file defines ONE document type
public int score;
public string player_name;

record Achievement {
  public int id;
  public string name;
  public datetime earned_at;
}

table<Achievement> achievements;

message AddAchievement {
  string name;
}

@connected {
  return true;
}

channel add_achievement(AddAchievement msg) {
  achievements <- { name: msg.name, earned_at: Time.datetime() };
}

When this code is deployed to a space (a collection of documents sharing the same code), each document instance is completely independent. Document game-123 knows nothing about document game-456. They might as well be on different planets.

Documents as Tiny Databases

Each document is essentially a small database containing:

  • Singleton fields: Top-level values like public int score
  • Tables: Collections of records like table<Card> deck
  • Computed values: Formulas and bubbles that derive from other data

The twist is that the document also contains the code that manipulates this data. There's no separation between "application server" and "database server." They're the same thing. And that turns out to matter a lot.

The Space Concept

A space is a namespace that holds many documents of the same type:

Space: "chess-games"
├── Document: "game-alice-bob-001"
├── Document: "game-carol-dan-002"
├── Document: "game-eve-frank-003"
└── ...

All documents in a space share the same Adama code, the same deployment configuration, and the same authentication rules. But each document has completely isolated state, its own connected clients, and independent state machine execution.

Note

Space names must be 4-127 characters, lowercase alphanumeric with hyphens. Document keys can be up to 511 characters with more flexibility.

The Delta Protocol

One of Adama's most powerful features -- and the one I'm probably proudest of -- is the delta protocol. It's how state gets efficiently synchronized to connected clients.

How It Works

  1. Initial sync: When a client connects, they receive a complete JSON snapshot of their personalized view

  2. Change detection: When document state changes, Adama computes what changed

  3. Delta transmission: Only the changes are sent to each client, personalized to their view

  4. Client merge: The client applies changes to their local state

public int x = 1;
public int y = 2;
public formula sum = x + y;

Initial state sent to client:

{"x": 1, "y": 2, "sum": 3}

After x changes to 5, client receives:

{"x": 5, "sum": 7}

Notice that y isn't in the delta because it didn't change. The client merges this delta with their existing state to get the complete picture. Simple, efficient, and it scales beautifully.

Array Handling

Arrays require special treatment because RFC 7386 (JSON Merge Patch) doesn't handle array modifications well. Adama uses a custom array delta format:

{
  "@o": ["1", "3", "5"],  // Ordering instruction
  "3": {"score": 42}      // Updated element
}

The @o field specifies which elements exist and their order. This allows adding elements at any position, removing elements, reordering elements, and updating individual element fields -- all without sending the entire array.

Per-Viewer Deltas

Here's where it gets interesting. Each connected client may see different data due to privacy rules, so Adama computes deltas separately for each viewer:

record Card {
  public int id;
  private principal owner;
  viewer_is<owner> int value;
}

table<Card> cards;

If Alice owns card 1 and Bob owns card 2:

  • Alice's view: [{"id": 1, "value": 7}, {"id": 2}]
  • Bob's view: [{"id": 1}, {"id": 2, "value": 3}]

When card 1's value changes, only Alice receives that delta. Bob's connection is unchanged. Privacy and efficiency working together.

Tip

The delta protocol means that large documents with small changes are cheap. A document with 10,000 records where one record changes sends only that one record's delta. The size of the document doesn't determine the cost of updates -- the size of the change does.

The Actor Model

Each Adama document behaves as an actor -- an independent entity that processes messages sequentially, maintains private state, communicates only through messages, and never shares memory with other actors.

Sequential Processing

Within a single document, all operations are sequential. If three clients send messages simultaneously, those messages are queued and processed one at a time:

Time -->

Client A sends msg1 ─┐
Client B sends msg2 ─┼─> [Queue] ─> Process msg1 ─> Process msg2 ─> Process msg3
Client C sends msg3 ─┘

This eliminates race conditions within a document. You never need to think about locks or concurrent access to document state. I cannot tell you how much pain this single design decision prevents.

Message Types

Documents process several types of messages:

Client messages - Sent by connected users

message PlaceBet {
  int amount;
}
channel bet(PlaceBet msg) {
  // Handle the bet
}

State machine transitions - Internal state changes

#dealing_cards {
  // Deal cards then transition
  transition #waiting_for_bets;
}

#waiting_for_bets {
}

Timed transitions - Scheduled execution

int time_remaining = 10;

#countdown {
  time_remaining--;
  if (time_remaining > 0) {
    transition #countdown in 1; // Run again in 1 second
  } else {
    transition #game_over;
  }
}

#game_over {
}

Connection events - Client connect/disconnect

@connected {
  // Someone connected
  return true;
}

@disconnected {
  // Someone disconnected
}

Atomic Transactions

Every message is processed as an atomic transaction. If anything goes wrong, the entire transaction rolls back:

public int score = 0;
int max_score = 100;

record Achievement {
  public string name;
}
table<Achievement> achievements;

message ComplexMsg {}

channel complex_operation(ComplexMsg msg) {
  // Step 1: Modify some state
  score += 10;

  // Step 2: Validate something
  if (score > max_score) {
    abort; // ROLLBACK - score is unchanged
  }

  // Step 3: More modifications
  achievements <- { name: "High Score" };
}

Either all changes commit, or none do. Clients never see partial state. This is one of those things where you don't appreciate it until you've spent a week debugging a partially-committed transaction in a traditional system.

Document Isolation

Documents cannot directly access other documents. This isolation enables scalability (documents can run on different machines), reliability (one document's failure doesn't affect others), and security (documents can't read each other's private data).

Note

While documents can't directly communicate, they can use external services to coordinate indirectly. This is an advanced pattern covered in the services documentation.

The Dungeon Master Pattern

This is one of my favorite parts of Adama. The document acts like a Dungeon Master in a tabletop RPG -- it controls the flow of the game, asks players for input when appropriate, and enforces the rules.

The Problem with Client-Driven Flow

In traditional web apps, clients decide what actions to take:

Client: "I want to draw 3 cards"
Server: "OK, here are 3 cards"
Client: "I want to play a card"
Server: "OK, card played"

But games have rules about when you can take actions. You can only draw cards at the start of your turn. You can only play after you've drawn. Certain cards change what actions are available. Tracking all this in client code is error-prone and insecure -- clients can lie.

Server Takes Control

Adama inverts the control flow. The server asks clients for input when appropriate:

message DrawCount {
  int count;
}

principal current_player;

// Define an incomplete channel - no handler code
channel<DrawCount> how_many_cards;

#player_turn {
  // Server asks the player how many cards they want
  future<DrawCount> request = how_many_cards.fetch(current_player);

  // Execution pauses until the player responds
  DrawCount response = request.await();

  // Move to next phase
  transition #play_phase;
}

#play_phase {
}

The fetch operation sends a signal to the client that input is needed, returns a future representing the eventual response, and await pauses execution until the response arrives.

Durability Across Time

Here's where it gets wild. This await can span days. If the server restarts, the document resumes exactly where it left off, still waiting for that response.

message ApprovalDecision {
  bool approved;
}

principal manager;
channel<ApprovalDecision> approval_channel;

#approval_workflow {
  // Send request to manager
  future<ApprovalDecision> decision = approval_channel.fetch(manager);

  // This await might take 3 days while the manager is on vacation
  ApprovalDecision result = decision.await();

  if (result.approved) {
    transition #process_request;
  } else {
    transition #request_denied;
  }
}

#process_request {
}

#request_denied {
}

This pattern is perfect for turn-based games with human-scale delays, approval workflows, multi-step wizards -- anything where "wait for human input" is part of the logic. The state machine just... waits. Patiently. Across reboots. Across deployments.

Warning

Unlike typical async/await in other languages, Adama's await persists across server restarts. The document's state machine is durable. This is intentional and it's what makes the Dungeon Master pattern possible.

Parallel Fetches

You can ask multiple clients simultaneously:

message Vote {
  bool approve;
}

principal player1;
principal player2;

channel<Vote> vote_channel;

int tally = 0;

#voting {
  // Ask all players to vote at the same time
  future<Vote> v1 = vote_channel.fetch(player1);
  future<Vote> v2 = vote_channel.fetch(player2);

  // Wait for all votes
  Vote r1 = v1.await();
  Vote r2 = v2.await();
  if (r1.approve) { tally++; }
  if (r2.approve) { tally++; }

  transition #reveal_results;
}

#reveal_results {
}

Players vote in parallel, but the document waits for all votes before proceeding. Clean, simple, durable.

Scaling Strategy

How does Adama handle growth? The answer falls right out of the document-centric model.

Horizontal Scaling Through Documents

Each document is independent. Documents can live on different machines. Adding capacity means adding machines. No cross-document coordination required.

Machine 1          Machine 2          Machine 3
├── game-001       ├── game-100       ├── game-200
├── game-002       ├── game-101       ├── game-201
└── game-003       └── game-102       └── game-202

When a machine gets full, new documents go to new machines. The router directs connections to the right machine. That's it.

Single-Document Limits

Within a single document, there are limits:

Resource Practical Limit
State size Megabytes to low gigabytes
Update rate Thousands per second
Connected clients Thousands per document

For most applications -- games, collaboration, chat rooms -- these limits are generous.

When Documents Get Big

If you need more than a single document can handle:

  1. Shard by natural boundaries - One document per game, per chat room, per user
  2. Use multiple documents - A lobby document and separate game documents
  3. External aggregation - Use external services for cross-document queries
Tip

The pattern is "many small documents" not "one big document." A chess platform has thousands of game documents, not one document containing all games. Get this right and scaling is almost free.

Infinite Read Scale

While writes go to the authoritative document, reads can scale independently. The document emits a change log, the change log replicates to multiple regions, and read replicas can serve any number of readers.

This works because the delta protocol produces a deterministic stream of changes that can be replayed anywhere.

The Execution Model

Understanding how code actually runs helps avoid surprises.

Single-Threaded Execution

Each document runs on a single thread. No data races possible within a document. No need for locks or synchronization. Deterministic execution order. This is a constraint that gives you freedom -- freedom from an entire class of bugs that haunt concurrent systems.

Event Loop

Documents process events in a loop:

1. Receive event (message, timer, connection)
2. Execute handler code
3. Compute deltas for connected clients
4. Persist changes to durable storage
5. Send deltas to clients
6. Wait for next event

Changes are only visible to clients after the transaction commits. There is no way to see "in-progress" state.

Deterministic Replay

Adama documents can be replayed from their change log. Given the same sequence of events, execution produces identical results. This enables time-travel debugging, state reconstruction, disaster recovery, and testing with recorded sessions.

Note

Randomness in Adama uses document-scoped random number generators that are also persisted, so replay produces identical "random" results. Even chaos is deterministic here.

With the foundations covered, you're ready to start building. Head to the Quick Start to write your first Adama application.

Previous Philosophy
Next Quickstart