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, andtrashcannot 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:
- MANUAL: Users explicitly add or remove bookmarks. The order of bookmarks is preserved in the
bookmark_idslist. - 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.