Chapter 13: Persistence and the Root Node#
In this chapter, we'll explore Jac's automatic persistence system and the fundamental concept of the root node. We'll build a simple counter application that demonstrates how Jac automatically maintains state when running as a service with a database backend.
What You'll Learn
- Understanding Jac's automatic persistence mechanism with jac serve
- The root node as the entry point for all persistent data
- State consistency across API requests and service restarts
- Building stateful applications with jac-cloud
What is Automatic Persistence?#
Traditional programming requires explicit database setup, connection management, and data serialization. Jac eliminates this complexity by making persistence a core language feature when running as a service. When you use jac serve
with a database backend, nodes and their connections automatically persist across requests and service restarts.
Persistence Requirements
- Database Backend: Persistence requires
jac serve
with a configured database - Service Mode:
jac run
executions are stateless and don't persist data - Root Connection: Nodes must be connected to root to persist
- API Context: Persistence works within the context of API endpoints
Persistence Benefits
- Zero Configuration: No manual database schema or ORM setup
- Automatic State: Data persists without explicit save/load operations
- Graph Integrity: Relationships between nodes are maintained
- Type Safety: Persistent data maintains type information
- Instant Recovery: Services resume exactly where they left off
Traditional vs Jac Persistence#
Persistence Comparison
# counter_api.py - Manual database setup required
from flask import Flask, jsonify, request
import sqlite3
import os
app = Flask(__name__)
class Counter:
def __init__(self):
self.db_path = "counter.db"
self.init_db()
def init_db(self):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS counter (
id INTEGER PRIMARY KEY,
value INTEGER
)
''')
cursor.execute('SELECT value FROM counter WHERE id = 1')
if not cursor.fetchone():
cursor.execute('INSERT INTO counter (id, value) VALUES (1, 0)')
conn.commit()
conn.close()
def get_value(self):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('SELECT value FROM counter WHERE id = 1')
value = cursor.fetchone()[0]
conn.close()
return value
def increment(self):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('UPDATE counter SET value = value + 1 WHERE id = 1')
conn.commit()
conn.close()
return self.get_value()
counter = Counter()
@app.route('/counter')
def get_counter():
return jsonify({"value": counter.get_value()})
@app.route('/counter/increment', methods=['POST'])
def increment_counter():
new_value = counter.increment()
return jsonify({"value": new_value})
if __name__ == "__main__":
app.run(debug=True)
# main.jac - No database setup needed
node Counter {
has value: int = 0;
def increment() -> int {
self.value += 1;
return self.value;
}
def get_value() -> int {
return self.value;
}
}
walker get_counter {
obj __specs__ {
static has auth: bool = False;
}
can get_counter_endpoint with `root entry {
counter_nodes = [root --> Counter];
if not counter_nodes {
counter = Counter();
root ++> counter;
} else {
counter = counter_nodes[0];
}
report {"value": counter.get_value()};
}
}
walker increment_counter {
obj __specs__ {
static has auth: bool = False;
}
can increment_counter_endpoint with `root entry {
counter_nodes = [root --> Counter];
if not counter_nodes {
counter = Counter();
root ++> counter;
} else {
counter = counter_nodes[0];
}
new_value = counter.increment();
report {"value": new_value};
}
}
Setting Up a Jac Cloud Project#
To demonstrate persistence, we need to create a proper jac-cloud project structure:
Project Structure
Let's create our counter application:
Complete Counter Project
# main.jac
node Counter {
has value: int = 0;
has created_at: str;
can increment() -> int {
self.value += 1;
return self.value;
}
can reset() -> int {
self.value = 0;
return self.value;
}
}
walker get_counter {
can get_counter_endpoint with `root entry {
counter_nodes = [root --> Counter];
if not counter_nodes {
counter = Counter(created_at="2024-01-15");
root ++> counter;
report {"value": 0, "status": "created"};
} else {
counter = counter_nodes[0];
report {"value": counter.value, "status": "existing"};
}
}
}
walker increment_counter {
can increment_counter_endpoint with `root entry {
counter_nodes = [root --> Counter];
if not counter_nodes {
counter = Counter(created_at="2024-01-15");
root ++> counter;
} else {
counter = counter_nodes[0];
}
new_value = counter.increment();
report {"value": new_value, "previous": new_value - 1};
}
}
walker reset_counter {
can reset_counter_endpoint with `root entry {
counter_nodes = [root --> Counter];
if counter_nodes {
counter = counter_nodes[0];
counter.reset();
report {"value": 0, "status": "reset"};
} else {
report {"value": 0, "status": "no_counter_found"};
}
}
}
The Root Node Concept#
The root node is Jac's fundamental concept for persistent data organization. When running with jac serve
, every request has access to a special root
node that serves as the entry point for all persistent graph structures.
Understanding Root Node#
Root Node Properties
- Request Context: Available in every API request when using jac serve
- Persistence Gateway: Starting point for all persistent data
- Graph Anchor: All persistent nodes must be reachable from root
- Automatic Creation: Exists automatically with database backend
- Transaction Boundary: Changes persist at the end of each request
Running the Service#
# Navigate to project directory
cd counter-app
# Install dependencies
pip install -r requirements.txt
# Start the service with database persistence
jac serve main.jac
# Service starts on http://localhost:8000
# API documentation available at http://localhost:8000/docs
Testing Persistence#
# First request - Create counter
curl -X POST http://localhost:8000/walker/get_counter \
-H "Content-Type: application/json" \
-d '{}'
# Response: {"returns": [{"value": 0, "status": "created"}]}
# Increment the counter
curl -X POST http://localhost:8000/walker/increment_counter \
-H "Content-Type: application/json" \
-d '{}'
# Response: {"returns": [{"value": 1, "previous": 0}]}
# Increment again
curl -X POST http://localhost:8000/walker/increment_counter \
-H "Content-Type: application/json" \
-d '{}'
# Response: {"returns": [{"value": 2, "previous": 1}]}
# Check counter value
curl -X POST http://localhost:8000/walker/get_counter \
-H "Content-Type: application/json" \
-d '{}'
# Response: {"returns": [{"value": 2, "status": "existing"}]}
# Restart the service (Ctrl+C, then jac serve main.jac again)
# Counter value persists after restart
curl -X POST http://localhost:8000/walker/get_counter \
-H "Content-Type: application/json" \
-d '{}'
# Response: {"returns": [{"value": 2, "status": "existing"}]}
Persistence in Action
Notice how the counter value persists between requests and even service restarts when using jac serve
with a database!
State Consistency#
Jac maintains state consistency through its graph-based persistence model when running as a service. All connected nodes and their relationships are automatically maintained across requests and service restarts.
Enhanced Counter with History#
Let's enhance our counter to track increment history:
Counter with History Tracking
# main.jac - Enhanced with history
import from datetime { datetime }
node Counter {
has created_at: str;
has value: int = 0;
def increment() -> int {
old_value = self.value;
self.value += 1;
# Create history entry
history = HistoryEntry(
timestamp=str(datetime.now()),
old_value=old_value,
new_value=self.value
);
self ++> history;
return self.value;
}
def get_history() -> list[dict] {
history_nodes = [self --> HistoryEntry];
return [
{
"timestamp": h.timestamp,
"old_value": h.old_value,
"new_value": h.new_value
}
for h in history_nodes
];
}
}
node HistoryEntry {
has timestamp: str;
has old_value: int = 0;
has new_value: int = 0;
}
walker get_counter_with_history {
obj __specs__ {
static has auth: bool = False;
}
can get_counter_with_history_endpoint with `root entry {
counter_nodes = [root --> Counter];
if not counter_nodes {
counter = Counter(created_at=str(datetime.now()));
root ++> counter;
report {
"value": 0,
"status": "created",
"history": []
};
} else {
counter = counter_nodes[0];
report {
"value": counter.value,
"status": "existing",
"history": counter.get_history()
};
}
}
}
walker increment_with_history {
obj __specs__ {
static has auth: bool = False;
}
can increment_with_history_endpoint with `root entry {
counter_nodes = [root --> Counter];
if not counter_nodes {
counter = Counter(created_at=str(datetime.now()));
root ++> counter;
} else {
counter = counter_nodes[0];
}
new_value = counter.increment();
report {
"value": new_value,
"history": counter.get_history()
};
}
}
Testing History Persistence#
# Start fresh service
jac serve main.jac
# Multiple increments to build history
curl -X POST http://localhost:8000/walker/increment_with_history \
-H "Content-Type: application/json" \
-d '{}'
curl -X POST http://localhost:8000/walker/increment_with_history \
-H "Content-Type: application/json" \
-d '{}'
curl -X POST http://localhost:8000/walker/increment_with_history \
-H "Content-Type: application/json" \
-d '{}'
# Check counter with complete history
curl -X POST http://localhost:8000/walker/get_counter_with_history \
-H "Content-Type: application/json" \
-d '{}'
# Response includes value and complete history array
# Restart service - history persists
# jac serve main.jac (after restart)
curl -X POST http://localhost:8000/walker/get_counter_with_history \
-H "Content-Type: application/json" \
-d '{}'
# All history entries remain intact
Building Stateful Applications#
The automatic persistence enables building sophisticated stateful applications. Let's create a multi-counter management system:
Multi-Counter Management System
# main.jac - Multi-counter system
import from datetime { datetime }
node CounterManager {
has created_at: str;
def create_counter(name: str) -> dict {
# Check if counter already exists
existing = [self --> Counter](?name == name);
if existing {
return {"status": "exists", "counter": existing[0].name};
}
new_counter = Counter(name=name, value=0);
self ++> new_counter;
return {"status": "created", "counter": name};
}
def list_counters() -> list[dict] {
counters = [self --> Counter];
return [
{"name": c.name, "value": c.value}
for c in counters
];
}
def get_total() -> int {
counters = [self --> Counter];
return sum([c.value for c in counters]);
}
}
node Counter {
has name: str;
has value: int = 0;
def increment(amount: int = 1) -> int {
self.value += amount;
return self.value;
}
}
walker create_counter {
has name: str;
obj __specs__ {
static has auth: bool = False;
}
can create_counter_endpoint with `root entry {
manager_nodes = [root --> CounterManager];
if not manager_nodes {
manager = CounterManager(created_at=str(datetime.now()));
root ++> manager;
} else {
manager = manager_nodes[0];
}
result = manager.create_counter(self.name);
report result;
}
}
walker increment_named_counter {
has name: str;
has amount: int = 1;
obj __specs__ {
static has auth: bool = False;
}
can increment_named_counter_endpoint with `root entry {
manager_nodes = [root --> CounterManager];
if not manager_nodes {
report {"error": "No counter manager found"};
return;
}
manager = manager_nodes[0];
counters = [manager --> Counter](?name == self.name);
if not counters {
report {"error": f"Counter {self.name} not found"};
return;
}
counter = counters[0];
new_value = counter.increment(self.amount);
report {"name": self.name, "value": new_value};
}
}
walker get_all_counters {
obj __specs__ {
static has auth: bool = False;
}
can get_all_counters_endpoint with `root entry {
manager_nodes = [root --> CounterManager];
if not manager_nodes {
report {"counters": [], "total": 0};
return;
}
manager = manager_nodes[0];
report {
"counters": manager.list_counters(),
"total": manager.get_total()
};
}
}
API Usage Examples#
# Create multiple counters
curl -X POST "http://localhost:8000/walker/create_counter" \
-H "Content-Type: application/json" \
-d '{"name": "page_views"}'
curl -X POST "http://localhost:8000/walker/create_counter" \
-H "Content-Type: application/json" \
-d '{"name": "user_signups"}'
# Increment specific counters
curl -X POST "http://localhost:8000/walker/increment_named_counter" \
-H "Content-Type: application/json" \
-d '{"name": "page_views", "amount": 5}'
curl -X POST "http://localhost:8000/walker/increment_named_counter" \
-H "Content-Type: application/json" \
-d '{"name": "user_signups", "amount": 2}'
# View all counters
curl -X POST http://localhost:8000/walker/get_all_counters \
-H "Content-Type: application/json" \
-d '{}'
# Response: {"returns": [{"counters": [{"name": "page_views", "value": 5}, {"name": "user_signups", "value": 2}], "total": 7}]}
Best Practices#
Persistence Guidelines
- Service mode only: Use
jac serve
for persistent applications, notjac run
- Connect to root: All persistent data must be reachable from root
- Initialize gracefully: Check for existing data before creating new instances
- Use proper IDs: Generate unique identifiers for nodes that need them
- Plan for concurrency: Consider multiple users accessing the same data
- Database configuration: Set up proper database connections for production
Key Takeaways#
What We've Learned
Persistence Fundamentals:
- Service requirement: Persistence only works with
jac serve
and database backends - Root connection: All persistent nodes must be connected to the root node
- Automatic behavior: Data persists without explicit save/load operations
- Request isolation: Each API request has access to the same persistent graph
Root Node Concept:
- Graph anchor: Starting point for all persistent data structures
- Request context: Available automatically in every API endpoint
- Transaction boundary: Changes persist at the end of each successful request
- State consistency: Maintains graph integrity across service restarts
State Management:
- Automatic persistence: Connected nodes survive service restarts
- Graph integrity: Relationships between nodes are maintained
- Type preservation: Node properties retain their types across persistence
- Concurrent access: Multiple requests can safely access the same data
Development Patterns:
- Initialization checks: Use filtering to find existing data before creating new
- Unique identification: Generate proper IDs for nodes that need them
- Data validation: Implement business rules at the application level
- Error handling: Graceful handling of missing or invalid data
Try It Yourself
Build persistent applications by creating: - A todo list API with persistent tasks - A blog system with posts and comments - An inventory management system - A user profile system with preferences
Remember: Only nodes connected to root (directly or indirectly) will persist when using jac serve
!
Ready to explore cloud deployment? Continue to Chapter 14: Jac Cloud Introduction!