Skip to main content

Overview of the Repository Layer

The repository layer in this project is centered around the BookmarkRepository class located in app/db/repository.py. It serves as the centralized data management hub, providing a unified interface for persisting and retrieving the application's core entities: Bookmarks, Tags, and Collections.

Architectural Role

The BookmarkRepository acts as the "Source of Truth" for the application's state. It is designed to isolate business logic from the specifics of data storage. In the current implementation, the repository is an in-memory store, but its interface defines how the rest of the system interacts with the data layer.

The repository is typically managed as a component of the BookmarkService singleton, which orchestrates high-level operations:

# 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)

Data Storage and Entities

The repository manages three primary entity types using internal Python dictionaries. The keys are entity IDs (strings), and the values are the model instances:

  • Bookmarks: Managed via self._bookmarks: Dict[str, Bookmark].
  • Tags: Managed via self._tags: Dict[str, Tag].
  • Collections: Managed via self._collections: Dict[str, Collection].

Because the storage is in-memory, all mutation methods (e.g., save_bookmark, delete_tag) persist changes immediately to these dictionaries. However, this also means that all data is volatile and will be lost when the application process restarts.

Core Operations and Querying

The repository provides standard CRUD operations for all three entities, along with specialized query methods.

Bookmark Management

Beyond basic retrieval by ID, the repository handles complex queries such as paginated listing and status-based filtering. The list_bookmarks method implements in-memory sorting and pagination:

# app/db/repository.py

def list_bookmarks(
self,
page: int = 1,
per_page: int = 25,
status: Optional[str] = None,
) -> Tuple[List[Bookmark], int]:
items = list(self._bookmarks.values())
if status:
try:
target = BookmarkStatus(status)
items = [b for b in items if b.status == target]
except ValueError:
pass
# Sort by creation date descending
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

It also supports cross-entity lookups, such as get_bookmarks_with_tag(tag_id), which is essential for cascading updates. For example, when a tag is deleted, the BookmarkService uses this method to find and update all associated bookmarks.

Tag and Collection Management

Tags and collections follow a similar pattern with save_*, get_*, delete_*, and list_* methods. These are used by the service layer to manage the organizational structure of the bookmarks.

The SearchIndex (in app/services/search_service.py) relies on the repository to bootstrap its inverted index. During initialization or a manual rebuild, the search service fetches all bookmarks from the repository to populate its index:

# app/services/search_service.py

def _rebuild(self) -> None:
"""Rebuild the entire index from the repository."""
self._index.clear()
# Fetch a large batch of bookmarks for indexing
all_bookmarks, _ = self._repo.list_bookmarks(page=1, per_page=10000)
for bookmark in all_bookmarks:
self.index_bookmark(bookmark)

Implementation Considerations

  • No Transaction Support: Mutations are applied immediately to the internal dictionaries. There is no built-in mechanism for rolling back changes if a multi-step operation (like updating a bookmark and its search index) fails partially.
  • In-Memory Performance: While extremely fast for small datasets, the list_bookmarks method performs a full sort and slice on every call. This approach is optimized for simplicity rather than scaling to millions of records.
  • Test Utilities: The repository includes a _clear_all() method and a _count_all() diagnostic helper. These are intended for use in test suites to ensure a clean state between test cases.