# How the Domain Layer Implements the Repository Pattern for SurrealDB Operations

> Learn how the Open-Notebook domain layer uses the repository pattern to manage SurrealDB operations. Discover clean CRUD methods and separation of concerns in this technical guide.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: how-to-guide
- Published: 2026-06-21

---

**The Open-Notebook domain layer separates business logic from SurrealDB driver specifics by delegating all database operations to repository helpers in [`open_notebook/database/repository.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/database/repository.py) while exposing clean CRUD methods through a generic `ObjectModel` base class.**

This architecture keeps the domain layer framework‑agnostic and testable. Instead of embedding SurrealQL queries inside business logic, the repository pattern centralizes connection handling, query construction, and result parsing in dedicated utilities, while domain models in `open_notebook/domain/` focus purely on validation and business rules.

## Repository Layer: Low‑Level SurrealDB Abstractions

All direct interaction with SurrealDB resides in [`open_notebook/database/repository.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/database/repository.py). This module exposes async helper functions that wrap the official driver, handle `RecordID` parsing, and manage timestamps.

### Query Execution and Record Handling

The `repo_query()` function executes arbitrary SurrealQL statements and transforms the raw response into plain Python dictionaries. Located at lines 65–73, it parses `RecordID` values to strings and returns clean data structures that the domain layer can consume without driver dependencies.

```python
from open_notebook.database.repository import repo_query

# Execute a custom SurrealQL statement

results = await repo_query("SELECT * FROM notebook WHERE name = 'Research'")

```

### Atomic CRUD Operations

The repository provides explicit functions for persistence operations, each handling specific SurrealDB commands:

- **`repo_create()`** (lines 85–99): Wraps `INSERT` statements and injects `created_at` timestamps
- **`repo_update()`** (lines 124–131): Handles `UPDATE` operations with error logging
- **`repo_upsert()`** (lines 135–144): Implements `UPSERT` for idempotent writes
- **`repo_delete()`** (lines 158–166): Executes `DELETE` commands with validation
- **`repo_relate()`** (lines 6–15): Creates graph edges between records using the `RELATE` statement

These functions ensure every database interaction includes proper error handling and timestamp management, eliminating repetitive boilerplate from domain models.

## Domain Layer: Generic CRUD via ObjectModel

Domain models inherit from `ObjectModel` in [`open_notebook/domain/base.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/base.py), which implements the repository pattern’s abstraction layer by delegating to the helpers above. Concrete entities only declare a `table_name` and field definitions to gain full persistence capabilities.

### Retrieval and Hydration

The `ObjectModel.get(id)` method at lines 102–124 constructs a table‑aware `SELECT` query, validates the record exists, and automatically instantiates the correct subclass via `_get_class_by_table_name`. For bulk operations, `ObjectModel.get_all(order_by)` at lines 38–50 builds a `SELECT * FROM <table>` query with optional ordering validation.

```python
from open_notebook.domain.notebook import Notebook

# Single record retrieval

notebook = await Notebook.get("notebook:abcd1234")

# List all notebooks ordered by creation date

notebooks = await Notebook.get_all(order_by="created_at DESC")

```

### Persistence Lifecycle

The `save()` method (lines 46–74) orchestrates the persistence workflow:

1. Validates the model instance
2. Prepares a dictionary of non‑null fields via `_prepare_save_data()` (lines 75–84)
3. Calls `repo_create()` for new objects or `repo_update()` for existing records
4. Copies the returned fields back onto the instance to reflect database state

```python
nb = Notebook(name="AI Safety Research", description="Collection of papers")
await nb.save()  # Calls repo_create() internally

print(nb.id)     # Now populated with RecordID string

```

### Relationship Management

Graph relationships are handled through `relate()` at lines 117–124, which wraps `repo_relate()` to create edges between entities. Domain models expose semantic methods like `add_to_notebook()` that internally call `relate()` with the appropriate edge type and direction.

## Concrete Implementations: Domain‑Specific Models

Concrete entities leverage the inherited CRUD behavior by defining minimal configuration. For example, `Notebook` in [`open_notebook/domain/notebook.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/notebook.py) (lines 16–18) declares only `table_name = "notebook"` and gains automatic persistence, retrieval, and deletion capabilities.

```python
class Notebook(ObjectModel):
    table_name: str = "notebook"
    name: str
    description: Optional[str] = None

```

This declarative approach means adding a new entity requires only field definitions and business logic, with zero boilerplate for database operations.

## Advanced Patterns: Complex Queries and Domain Logic

Higher‑level domain methods combine repository calls to implement rich operations. In `Notebook.get_sources()` (lines 29–42), the method executes a `repo_query()` with a `FETCH` clause to retrieve related sources, then hydrates `Source` objects from the raw rows.

```python

# Fetch notebook with related sources

notebook = await Notebook.get("notebook:xyz789")
sources = await notebook.get_sources(include_full_text=False)

for src in sources:
    print(src.title)

```

Similar patterns appear throughout the domain, such as `Source.get_insights()` and `ChatSession.relate_to_notebook()`, demonstrating how the repository pattern enables complex business logic while keeping SurrealQL encapsulated in the infrastructure layer.

## Summary

- **Repository isolation**: All SurrealDB driver code lives in [`open_notebook/database/repository.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/database/repository.py), providing `repo_query()`, `repo_create()`, `repo_update()`, and `repo_relate()` as the sole database interaction points.
- **Domain abstraction**: `ObjectModel` in [`open_notebook/domain/base.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/base.py) implements generic CRUD by delegating to repository functions, allowing concrete models to inherit persistence behavior automatically.
- **Minimal configuration**: Domain entities like `Notebook` declare only `table_name` and fields, with business logic completely decoupled from SurrealQL syntax.
- **Relationship support**: Graph edges are created via `ObjectModel.relate()`, enabling rich domain relationships without exposing the underlying `RELATE` statement complexity.
- **Testability**: The separation allows unit testing domain logic with mocked repository functions, ensuring business rules remain independent of database availability.

## Frequently Asked Questions

### What is the repository pattern and why does Open-Notebook use it for SurrealDB?

The repository pattern abstracts data access by placing all database operations behind a collection of helper functions. Open‑Notebook uses this pattern to prevent SurrealDB‑specific code (SurrealQL, `RecordID` handling, connection management) from leaking into business logic. This keeps the domain layer agnostic to the underlying database technology, making it easier to test, maintain, or potentially migrate to different storage backends.

### How does `ObjectModel` handle database record IDs?

`ObjectModel` automatically manages SurrealDB `RecordID` objects through the repository layer. When `repo_query()` executes at lines 65–73, it parses `RecordID` values to strings before returning them to the domain layer. The `get()` method at lines 102–124 uses these string IDs to construct table‑aware queries, while `save()` updates the instance’s `id` field after creation to reflect the database‑generated identifier.

### Can I use the repository functions directly without inheriting from `ObjectModel`?

Yes. The repository functions in [`open_notebook/database/repository.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/database/repository.py) are designed as standalone async utilities that can be imported and used anywhere in the application. While `ObjectModel` provides convenient CRUD methods for standard entities, direct repository access is appropriate for complex queries, bulk operations, or transactions that don’t map cleanly to single‑model persistence patterns.

### How are relationships between entities implemented in the domain layer?

Relationships are created through the `relate()` method at lines 117–124 of [`open_notebook/domain/base.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/base.py), which delegates to `repo_relate()` to execute SurrealDB’s `RELATE` statement. Domain models expose semantic wrapper methods (like `Source.add_to_notebook()`) that call `relate()` with the appropriate edge type, keeping the graph structure encapsulated while providing a clean, object‑oriented API for linking entities.