Skip to main content

Domain Models

The domain models in this project represent the core entities of the bookmarking system: Bookmarks, Tags, and Collections. These models are implemented as Python dataclasses, providing a clean, type-hinted structure that combines data storage with internal state logic and serialization.

Bookmark Model

The Bookmark class (defined in app/models/bookmark.py) is the central entity. It tracks a URL, its metadata, and its organizational state.

State Management

A bookmark's lifecycle is managed through the BookmarkStatus enum, which includes ACTIVE, ARCHIVED, and TRASHED. The model provides explicit methods for these transitions, ensuring that the updated_at timestamp is always refreshed via an internal _touch() helper.

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

def trash(self) -> None:
"""Soft-delete the bookmark by moving it to the trash."""
self.status = BookmarkStatus.TRASHED
self._touch()

def _touch(self) -> None:
"""Update the modification timestamp."""
self.updated_at = datetime.utcnow()

Serialization and IDs

The model uses uuid.uuid4().hex[:12] for its unique identifier. Serialization is handled by to_dict() for API responses and from_dict() for instantiation from request data. Notably, from_dict() is designed for creation; it only extracts a subset of fields (url, title, description, tags), while fields like id and created_at are generated by the dataclass defaults.

Tag Model

The Tag class (in app/models/tag.py) provides a way to label bookmarks. It includes a TagColor enum for UI customization and tracks its own usage.

Usage Tracking

Unlike bookmarks, tags maintain a usage_count that is incremented or decremented as bookmarks are associated or disassociated. This allows the system to quickly identify popular or unused tags without performing expensive joins across the entire bookmark repository.

def increment_usage(self) -> int:
"""Record that a bookmark now uses this tag. Returns new count."""
self.usage_count += 1
return self.usage_count

Validation and Normalization

Tags have strict naming rules enforced by _validators.py. They cannot be empty, exceed 50 characters, or use reserved names like all, untagged, archived, or trash. The model also provides a _normalize_name() method to facilitate case-insensitive uniqueness checks.

Collection Model

The Collection class (in app/models/collection.py) allows for grouping bookmarks. It supports two distinct behaviors defined by CollectionType:

  1. MANUAL: Users explicitly add or remove bookmark IDs.
  2. SMART: Bookmarks are dynamically included based on a filter_rule.

Smart Collection Logic

Smart collections use a naive substring match against bookmark titles and descriptions. This logic is encapsulated in the _apply_filter method, which is typically called by the service layer to resolve the collection's contents.

def _apply_filter(self, bookmarks: list) -> List[str]:
"""Evaluate the filter_rule against a list of bookmarks."""
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()]

Ordering

For manual collections, the bookmark_ids list maintains a specific order. The reorder() method allows updating this sequence but enforces a constraint: the new list must contain exactly the same set of IDs as the current one, preventing accidental additions or removals during a reorder operation.

Design Decisions and Tradeoffs

ID Generation Inconsistency

The system uses truncated UUIDs for identifiers, but the lengths vary by entity:

  • Bookmarks: 12 characters (uuid.uuid4().hex[:12])
  • Tags: 8 characters (uuid.uuid4().hex[:8])
  • Collections: 10 characters (uuid.uuid4().hex[:10])

While this reduces the string length for URLs and APIs, it increases the theoretical risk of collisions compared to full UUIDs, especially for tags which have the shortest IDs.

Validation Placement

Validation is split between the models and a private _validators.py module. While models like Tag perform some internal checks (e.g., in rename()), the BookmarkService typically calls the standalone validator functions before even instantiating the model. This ensures that invalid data never enters the domain layer.

Serialization Strategy

The use of to_dict() and from_dict() provides a manual but explicit mapping for JSON serialization. This avoids dependencies on complex serialization libraries but requires manual updates to these methods whenever a new field is added to the dataclass. For example, Bookmark.from_dict intentionally ignores the id field, which is appropriate for creating new bookmarks but would require a different approach if used for restoring bookmarks from a database.