Skip to content

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

counter-app/
├── .env                 # Environment configuration
├── main.jac            # Main application logic
├── server.py           # Optional custom server setup
└── requirements.txt    # Python dependencies

Let's create our counter application:

Complete Counter Project

# .env - Database configuration
DATABASE_URL=sqlite:///./app.db
SECRET_KEY=your-secret-key-here
# 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"};
        }
    }
}
jaclang
fastapi
uvicorn
python-dotenv

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, not jac 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!