Skip to main content

Managing Bookmark Lifecycle

To manage the lifecycle of bookmarks—including creation, retrieval, updates, and soft-deletes—you use the BookmarkService. This service acts as a singleton facade that orchestrates operations across the repository, search index, and cache.

Accessing the Service

The BookmarkService is implemented as a singleton. You should instantiate it at the module level to share state across your application components.

from app.services.bookmark_service import BookmarkService

# Obtain the singleton instance
service = BookmarkService()

Creating a Bookmark

To create a bookmark, pass a dictionary containing at least a url and a title. The service performs validation and returns a tuple containing the created Bookmark object and an error message (if any).

data = {
"url": "https://example.com",
"title": "Example Domain",
"description": "A useful example site"
}

bookmark, error = service.create_bookmark(data)

if error:
# Handle validation errors (e.g., invalid URL or empty title)
print(f"Failed to create bookmark: {error}")
else:
print(f"Created bookmark with ID: {bookmark.id}")

When a bookmark is created, the service automatically:

  1. Validates the url and title.
  2. Persists the entity via BookmarkRepository.
  3. Indexes the content in the SearchIndex.
  4. Invalidates any existing cache entry for that ID.

Retrieving Bookmarks

You can retrieve a single bookmark by its ID or fetch a paginated list with optional status filtering.

Single Retrieval (Cached)

The get_bookmark method checks the internal LRUCache before querying the repository.

bookmark_id = "some-uuid"
bookmark = service.get_bookmark(bookmark_id)

if not bookmark:
print("Bookmark not found")

Paginated Listing and Filtering

Use list_bookmarks to retrieve sets of bookmarks. You can filter by status using the strings "active", "archived", or "trashed".

# Get the first page of archived bookmarks
bookmarks, total_count = service.list_bookmarks(
page=1,
per_page=25,
status="archived"
)

print(f"Found {total_count} archived bookmarks.")

Updating and State Transitions

The service provides methods for partial updates and explicit state transitions like archiving or restoring.

Partial Updates

The update_bookmark method accepts a dictionary of fields to change. It automatically updates the updated_at timestamp and refreshes the search index.

updates = {"title": "New Improved Title"}
bookmark, error = service.update_bookmark(bookmark_id, updates)

if error:
print(f"Update failed: {error}")

Archiving and Restoring

State transitions are handled by specific methods that manage the underlying entity status and cache invalidation.

# Move to archive
archived_bookmark = service.archive_bookmark(bookmark_id)

# Restore to active status (from archive or trash)
restored_bookmark = service.restore_bookmark(bookmark_id)

Soft-Deleting Bookmarks

In this project, delete_bookmark performs a soft-delete. It moves the bookmark to the "trash" status rather than removing it from the database.

success = service.delete_bookmark(bookmark_id)

if not success:
print("Could not delete: Bookmark does not exist")

Troubleshooting

Shared State in Tests

Because BookmarkService is a singleton, state persists between test cases. If you are writing unit tests, use the internal _reset() method in your setup or teardown to ensure a clean environment.

def setup_function():
service = BookmarkService()
service._reset() # Reinitializes repo, cache, and search index

Validation Failures

The service does not raise exceptions for invalid data. Instead, it follows a Go-like return pattern: (Result, Error). Always check if the second element of the tuple is not None before proceeding with the result.

Tag Side Effects

When you delete a tag using service.delete_tag(tag_id), the service iterates through every bookmark containing that tag to remove the reference and invalidates their cache entries. This can be a heavy operation if many bookmarks share the same tag.