Service Layer
To manage business logic and orchestrate data across the repository, cache, and search index, use the BookmarkService facade.
The BookmarkService (found in app/services/bookmark_service.py) acts as the central entry point for all operations. It ensures that when you create or update a bookmark, the data is validated, the search index is updated, and the cache is invalidated.
from flask import request, jsonify
from app.services.bookmark_service import BookmarkService
# BookmarkService is a singleton
_service = BookmarkService()
def create_bookmark():
# 1. Get data from request
data = request.get_json(force=True)
# 2. Delegate to service layer
# Returns Tuple[Optional[Bookmark], Optional[str]]
bookmark, error = _service.create_bookmark(data)
# 3. Handle validation errors
if error:
return jsonify({"error": error}), 400
return jsonify(bookmark.to_dict()), 201
Core Orchestration Logic
The BookmarkService implements a singleton pattern using __new__ to ensure state (like the in-memory repository and search index) is shared across the application. When initialized, it bootstraps three internal components:
BookmarkRepository: The persistence layer for bookmarks, tags, and collections.LRUCache: A least-recently-used cache (default size 256) used to speed upget_bookmarklookups.SearchIndex: An in-memory inverted index that tokenizes titles and descriptions for full-text search.
Updating Bookmarks and Cache Invalidation
When updating a bookmark via update_bookmark, the service performs validation using internal helpers from app.models._validators, updates the repository, re-indexes the content for search, and invalidates the cache entry to prevent stale data.
def update_bookmark(self, bookmark_id: str, data: Dict[str, Any]) -> Tuple[Optional[Bookmark], Optional[str]]:
bookmark = self._repo.get_bookmark(bookmark_id)
if not bookmark:
return None, None
# Validation logic
if "title" in data:
err = _validate_title(data["title"])
if err: return None, err
bookmark.title = data["title"]
# Persistence and Orchestration
bookmark._touch()
self._repo.save_bookmark(bookmark)
self._search.index_bookmark(bookmark) # Update search index
self._cache.invalidate(bookmark.id) # Clear stale cache
return bookmark, None
Cascading Deletes for Tags
The service layer handles cross-entity consistency. For example, when a tag is deleted via delete_tag, the service must strip that tag from all associated bookmarks before removing the tag itself.
def delete_tag(self, tag_id: str) -> bool:
tag = self._repo.get_tag(tag_id)
if not tag:
return False
# Cascade: Remove tag from all bookmarks
for bookmark in self._repo.get_bookmarks_with_tag(tag_id):
bookmark.remove_tag(tag_id)
self._repo.save_bookmark(bookmark)
self._cache.invalidate(bookmark.id)
self._repo.delete_tag(tag_id)
return True
Full-Text Search Integration
The SearchIndex (in app/services/search_service.py) provides ranked search results. The service layer exposes this via the search method, which returns a list of Bookmark objects ordered by relevance.
# Usage in a route or internal logic
results = _service.search(query="python tutorial", limit=10)
The SearchIndex works by:
- Tokenizing text and removing stop words (e.g., "the", "and", "or").
- Mapping tokens to bookmark IDs in an inverted index.
- Ranking results based on the frequency of query tokens in the bookmark's title and description.
Troubleshooting and Gotchas
- Singleton State: Because
BookmarkServiceis a singleton, its internal state (including the repository and search index) is shared across the entire process. In a multi-process environment (like Gunicorn with multiple workers), each worker will have its own independent state. - Search Index Rebuild: The
SearchIndexis rebuilt entirely from the repository on startup (SearchIndex._rebuild). If the dataset is large, this may cause a delay during application initialization. - Tag Deletion Performance:
delete_tagperforms a linear scan of bookmarks associated with that tag. While efficient for small sets, this can become a bottleneck if a single tag is applied to thousands of bookmarks. - Validation Errors: Service methods typically return a
Tuple[Optional[Entity], Optional[str]]. Always check the second element for an error message before proceeding with the result.