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.
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
-
Initial sync: When a client connects, they receive a complete JSON snapshot of their personalized view
-
Change detection: When document state changes, Adama computes what changed
-
Delta transmission: Only the changes are sent to each client, personalized to their view
-
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.
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).
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.
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:
- Shard by natural boundaries - One document per game, per chat room, per user
- Use multiple documents - A lobby document and separate game documents
- External aggregation - Use external services for cross-document queries
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.
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.