Skip to main content

Domain Model Architecture

The domain model in this system is built around three primary entities: Bookmarks, Collections, and Tags. These entities work together to provide a flexible and structured way to organize web content. The BookmarkService in app/services/bookmark_service.py acts as the primary orchestrator, ensuring that operations across these entities maintain data integrity.

The Bookmark Entity

The Bookmark class (defined in app/models/bookmark.py) is the central entity of the system. It represents a saved URL along with its metadata and organizational state.

Lifecycle and Status

A bookmark's lifecycle is managed through the BookmarkStatus enum, which includes:

  • ACTIVE: The default state for new bookmarks.
  • ARCHIVED: For bookmarks that are no longer needed but should be kept.
  • TRASHED: For bookmarks marked for deletion.

The Bookmark class provides explicit methods to transition between these states:

def archive(self) -> None:
"""Move the bookmark to the archive."""
self.status = BookmarkStatus.ARCHIVED
self._touch()

def trash(self) -> None:
"""Move the bookmark to the trash."""
self.status = BookmarkStatus.TRASHED
self._touch()

The private _touch() method is called automatically during state changes to update the updated_at timestamp.

Identity and Metadata

Each bookmark is identified by a 12-character hex UUID (e.g., 5f3a1b2c4d5e). Beyond the standard url, title, and description, the Bookmark entity includes a metadata dictionary for storing arbitrary key/value pairs, allowing for future extensibility without schema changes.

Organizing with Tags

Tags provide a flat, flexible labeling system. The Tag class in app/models/tag.py manages these labels, which are identified by an 8-character hex UUID.

Normalization and Constraints

To prevent duplicate or confusing labels, tag names are normalized (stripped and lowercased) and validated against a set of reserved names. The _validate_tag_name function in app/models/_validators.py enforces these rules:

  • Reserved Names: all, untagged, archived, and trash cannot be used as tag names.
  • Length: Tag names must be 50 characters or fewer.

Relationship with Bookmarks

Bookmarks maintain a list of tag IDs. When a tag is added or removed, the Bookmark entity updates its internal list and triggers a timestamp update:

def add_tag(self, tag_id: str) -> bool:
"""Add a tag to the bookmark."""
if tag_id not in self.tags:
self.tags.append(tag_id)
self._touch()
return True
return False

Grouping with Collections

Collections (defined in app/models/collection.py) provide a way to group bookmarks. They are identified by a 10-character hex UUID.

Manual vs. Smart Collections

The system supports two types of collections via the CollectionType enum:

  1. MANUAL: Users explicitly add or remove bookmarks. The order of bookmarks is preserved in the bookmark_ids list.
  2. SMART: Bookmarks are dynamically included based on a filter_rule.

Smart Collection Logic

Smart collections use the _apply_filter method to determine which bookmarks belong to them. This logic typically filters bookmarks based on keywords found in their title or description:

def _apply_filter(self, bookmarks: list) -> List[str]:
if not self.filter_rule:
return []
keyword = self.filter_rule.lower()
return [b.id for b in bookmarks if keyword in b.title.lower() or keyword in b.description.lower()]

Note that add_bookmark will return False if called on a smart collection, as its membership is strictly rule-based.

System Orchestration

While individual models handle their own state, the BookmarkService manages complex interactions that span multiple entities.

Cross-Entity Integrity

A key example of this orchestration is tag deletion. When a tag is deleted, the service must ensure it is removed from all associated bookmarks and that the cache is invalidated:

def delete_tag(self, tag_id: str) -> bool:
"""Delete a tag and strip it from all bookmarks."""
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)
self._cache.invalidate(bookmark.id)
self._repo.delete_tag(tag_id)
return True

This pattern ensures that the domain remains consistent even when entities are removed or modified.