Skip to main content

Overview of the Repository Pattern

The Repository pattern in etchblok-test-api provides a centralized data access layer that decouples business logic from the underlying storage mechanism. By using the BookmarkRepository, the application ensures that all data operations for bookmarks, tags, and collections are handled consistently, regardless of how the data is physically stored.

Centralized Data Access

The BookmarkRepository class in app/db/repository.py serves as the single source of truth for the application's data. It manages three primary entities:

  • Bookmarks: The core domain entity representing saved URLs.
  • Tags: Labels used to categorize bookmarks.
  • Collections: Groups of bookmarks.

In etchblok-test-api, the repository is implemented using in-memory storage. When you instantiate BookmarkRepository, it initializes private dictionaries to hold these entities:

class BookmarkRepository:
def __init__(self) -> None:
self._bookmarks: Dict[str, Bookmark] = {}
self._tags: Dict[str, Tag] = {}
self._collections: Dict[str, Collection] = {}

Because this implementation is in-memory, all mutation methods persist changes immediately to these dictionaries. However, this also means that all data is volatile and will be lost if the application process restarts.

Managing Bookmarks

The repository provides standard CRUD operations for bookmarks. When you save a bookmark using save_bookmark(bookmark), it either inserts a new record or updates an existing one based on the bookmark.id.

Pagination and Filtering

To handle large sets of data efficiently, the list_bookmarks method supports pagination and status filtering. This is particularly useful when building user interfaces that need to display bookmarks in chunks.

def list_bookmarks(
self,
page: int = 1,
per_page: int = 25,
status: Optional[str] = None,
) -> Tuple[List[Bookmark], int]:
items = list(self._bookmarks.values())

# Filter by status (active, archived, trashed)
if status:
try:
target = BookmarkStatus(status)
items = [b for b in items if b.status == target]
except ValueError:
pass

# Sort by creation date (newest first)
items.sort(key=lambda b: b.created_at, reverse=True)

total = len(items)
start = (page - 1) * per_page
return items[start : start + per_page], total

The method returns a tuple containing the requested page of items and the total count of matching items, allowing the caller to calculate total pages for pagination controls.

Cross-Entity Lookups

The repository also facilitates relationships between entities. For example, get_bookmarks_with_tag(tag_id) allows you to retrieve all bookmarks associated with a specific tag by inspecting the tags list on each Bookmark object:

def get_bookmarks_with_tag(self, tag_id: str) -> List[Bookmark]:
"""Return all bookmarks that have a specific tag attached."""
return [b for b in self._bookmarks.values() if tag_id in b.tags]

Integration with Services

The BookmarkRepository is not typically called directly by route handlers. Instead, it is consumed by the BookmarkService in app/services/bookmark_service.py, which acts as a facade. The service handles higher-level concerns like validation, cache invalidation, and orchestrating multiple repository calls.

When BookmarkService is initialized, it creates a singleton instance of the repository:

# app/services/bookmark_service.py

def _init_services(self) -> None:
"""Bootstrap repository, cache, and search index."""
self._repo = BookmarkRepository()
self._cache: LRUCache[Bookmark] = LRUCache(max_size=256)
self._search = SearchIndex(self._repo)

The SearchIndex also receives a reference to the repository, allowing it to fetch full bookmark objects when performing full-text searches.

Implementation Considerations

While the Repository pattern abstracts the storage, the current in-memory implementation in etchblok-test-api has specific behaviors to keep in mind:

  • Immediate Persistence: Mutations are applied instantly to the internal dictionaries.
  • No Transactions: There is no support for atomic transactions across multiple operations. If a complex operation fails halfway through, previous changes remain in the repository.
  • In-Memory Scaling: Methods like list_bookmarks perform sorting and slicing in memory. While efficient for the small-to-medium datasets expected in this test API, this approach would require optimization (e.g., database-level indexing and limit/offset) if transitioned to a persistent database.
  • Test Helpers: The repository includes a _clear_all() method specifically for wiping data between test runs, ensuring a clean state for automated testing.