In-Memory Storage Design Decisions
The BookmarkRepository in app/db/repository.py serves as the primary data access layer for the application. While the current implementation is entirely in-memory, the design follows the Repository Pattern to decouple the business logic in the service layer from the underlying storage mechanism.
In-Memory Persistence Strategy
The repository maintains state using standard Python dictionaries for its three core entities: bookmarks, tags, and collections. This choice prioritizes development speed and simplicity for testing.
class BookmarkRepository:
def __init__(self) -> None:
self._bookmarks: Dict[str, Bookmark] = {}
self._tags: Dict[str, Tag] = {}
self._collections: Dict[str, Collection] = {}
However, this introduces a significant constraint: data volatility. All stored information is lost when the application process terminates. To mitigate this during the development phase, the BookmarkService initializes the repository as a singleton-like component within its _init_services method, ensuring a consistent state throughout the application's lifecycle.
Immediate Mutation Behavior
Unlike traditional database repositories that might use a "Unit of Work" pattern or explicit transaction commits, BookmarkRepository performs immediate mutations. Every save_* method directly updates the internal dictionary.
def save_bookmark(self, bookmark: Bookmark) -> None:
"""Insert or update a bookmark."""
self._bookmarks[bookmark.id] = bookmark
This design simplifies the implementation but places the responsibility for cross-entity consistency on the caller. For example, in BookmarkService.delete_tag, the service must manually iterate through bookmarks to remove the tag reference before deleting the tag itself from the repository:
# app/services/bookmark_service.py
def delete_tag(self, tag_id: str) -> bool:
tag = self._repo.get_tag(tag_id)
if not tag:
return False
for bookmark in self._repo.get_bookmarks_with_tag(tag_id):
bookmark.remove_tag(tag_id)
self._repo.save_bookmark(bookmark) # Immediate update
self._cache.invalidate(bookmark.id)
self._repo.delete_tag(tag_id) # Immediate update
return True
Pagination and Performance Tradeoffs
Because the data is stored in-memory, the repository must handle operations like sorting and pagination manually. In list_bookmarks, the repository converts the entire dictionary of bookmarks into a list, sorts them by creation date, and then slices the list based on the requested page.
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
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
While efficient for small datasets, this approach has an $O(N \log N)$ complexity for every request due to the sort operation. In a persistent database implementation, this would typically be offloaded to the database engine using ORDER BY and LIMIT/OFFSET clauses.
Future Migration Path
The current implementation is explicitly designed as a placeholder for a persistent database. The class docstring notes: "For a real database you'd add transaction support here."
The transition to a database like SQLite or PostgreSQL is facilitated by several design choices:
- Interface Stability: The
BookmarkServiceinteracts only with the public methods ofBookmarkRepository. As long as a new SQL-based repository implements these methods, the service layer remains unchanged. - Internal Helpers: Methods like
_clear_alland_count_allare provided for diagnostics and testing, allowing the test suite to remain agnostic of the storage backend. - Search Integration: The
SearchIndexinapp/services/search_service.pydepends on the repository to rebuild its index. By maintaining thelist_bookmarksinterface, the search engine can continue to function regardless of whether the data comes from memory or a disk-based table.
Hard vs. Soft Deletion
A notable design distinction exists between the repository and the service layer regarding data removal. The BookmarkRepository performs hard deletes—actually removing the object from the dictionary:
def delete_bookmark(self, bookmark_id: str) -> bool:
"""Hard-delete a bookmark. Returns True if it existed."""
return self._bookmarks.pop(bookmark_id, None) is not None
In contrast, the BookmarkService implements soft deletion (moving to "trash") by updating the bookmark's status and saving it back to the repository. The repository's delete_bookmark method is typically reserved for permanent removal or cleanup operations.