Skip to main content

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:

  1. BookmarkRepository: The persistence layer for bookmarks, tags, and collections.
  2. LRUCache: A least-recently-used cache (default size 256) used to speed up get_bookmark lookups.
  3. 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 BookmarkService is 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 SearchIndex is 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_tag performs 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.