ADR-041: Domain Primitives - Item and List Refactoring

Status

✅ Implemented (November 2025)

Context

Original Vision

The original Piper Morgan architecture envisioned Item and List as cognitive primitives - universal concepts that all specific list types would extend. Todos were intended to be one specialization of Item, enabling future support for shopping lists, reading lists, project lists, etc.

Problem

Over time, the implementation diverged from this vision:

Opportunity

With the codebase stabilizing, this was the right time to implement the original architectural vision and create the foundation for future extensibility.

Decision

We refactored the domain model to implement polymorphic inheritance with Item and List as universal primitives.

Architecture

Domain Model (Cognitive Primitives):

class Item:
    """Universal base class for all list items."""
    id: UUID
    text: str           # Universal property
    position: int       # Order in list
    list_id: UUID      # Which list contains this
    created_at: datetime
    updated_at: datetime

class Todo(Item):
    """Todo is an Item that can be completed."""
    # Inherits: id, text, position, list_id, timestamps
    # Adds:
    priority: str
    status: str
    completed: bool
    due_date: Optional[datetime]
    # ... plus 20+ other todo-specific fields

Database Model (Polymorphic Inheritance):

class ItemDB(Base):
    """Base table for all items (joined table inheritance)."""
    __tablename__ = "items"

    id = Column(String, primary_key=True)
    text = Column(String, nullable=False)
    position = Column(Integer, nullable=False, default=0)
    list_id = Column(String, ForeignKey("lists.id"))
    item_type = Column(String)  # Discriminator

    __mapper_args__ = {
        "polymorphic_on": item_type,
        "polymorphic_identity": "item"
    }

class TodoDB(ItemDB):
    """Todo-specific table joined to items."""
    __tablename__ = "todo_items"

    id = Column(String, ForeignKey("items.id"), primary_key=True)
    # 24 todo-specific fields (priority, status, etc.)

    __mapper_args__ = {
        "polymorphic_identity": "todo"
    }

Service Layer (Universal Operations):

class ItemService:
    """Universal operations for any item type."""
    async def create_item(text, list_id, item_class, **kwargs) -> Item
    async def get_item(item_id, item_class) -> Optional[Item]
    async def update_item_text(item_id, new_text) -> Optional[Item]
    async def reorder_items(list_id, item_ids) -> List[Item]
    async def delete_item(item_id) -> bool
    async def get_items_in_list(list_id, item_type) -> List[Item]

class TodoService(ItemService):
    """Todo-specific operations."""
    # Inherits: create, get, update, reorder, delete
    async def create_todo(...) -> Todo
    async def complete_todo(todo_id) -> Todo
    async def reopen_todo(todo_id) -> Todo
    async def set_priority(todo_id, priority) -> Todo

Implementation

Phase 0: Pre-Flight Checklist (25 minutes)

Phase 1: Create Primitives (75 minutes)

Phase 2: Refactor Todo (6 hours across 2 days)

Phase 3: Universal Services (1 hour)

Phase 4: Integration and Polish (45 minutes)

Consequences

Positive

  1. Extensibility
    • Adding new item types (ShoppingItem, ReadingItem) is trivial
    • Just extend Item/ItemDB/ItemService
    • Inherit all universal operations for free
    • Pattern scales to any number of item types
  2. Code Reuse
    • Generic operations (create, update, reorder, delete) work on all types
    • No duplication across item types
    • Service layer provides clean abstraction
    • Estimated 70% code reuse for new item types
  3. Type Safety
    • Polymorphic inheritance ensures type correctness
    • SQLAlchemy handles joined table queries automatically
    • Type discrimination via item_type field
    • Python type hints throughout
  4. Backward Compatibility
    • title property maps to text (old code works)
    • API contracts maintained
    • Zero breaking changes for existing code
    • Seamless migration path
  5. Clean Architecture
    • Clear separation: API → Service → Repository → Database
    • Universal operations in ItemService
    • Type-specific operations in subclasses (TodoService)
    • Proper separation of concerns
  6. Performance
    • Proper indexes on both tables (14 indexes total)
    • Efficient joined queries via SQLAlchemy
    • Polymorphic queries optimized
    • No performance degradation observed
  7. Testability
    • 92+ comprehensive tests (37+66+16+10)
    • 100% test pass rate
    • Integration tests verify end-to-end
    • Easy to mock services for unit tests

Negative

  1. Complexity ⚠️
    • Polymorphic inheritance adds conceptual complexity
    • Developers need to understand joined table inheritance
    • Learning curve for new team members
    • Mitigation: Comprehensive documentation, ADR, tests, examples
  2. Query Performance ⚠️
    • Joined queries slightly slower than single table
    • Two table reads instead of one for todos
    • Impact: Minimal (<5ms overhead, proper indexes)
    • Mitigation: Monitoring, optimization if needed, caching layer possible
  3. Migration Risk ⚠️
    • Data migration required for existing todos
    • Database schema changes
    • Result: Migration successful, zero data loss, <1 minute execution
  4. Async Lazy Loading ⚠️
    • SQLAlchemy polymorphic queries can have async issues with lazy loading
    • Impact: Rare edge case when mixing item types
    • Mitigation: Use type filters, query specific types

Trade-offs

Alternative 1: Keep Separate Tables

Alternative 2: Single Table Inheritance

Alternative 3: Class Table Inheritance (Chosen) ✅

Validation

Test Coverage

Performance

Extensibility Proven

Integration Verified

Technical Details

Database Schema

Before:

todos table (standalone, 30+ columns)
├── id, title, description, priority, status, ...

After:

items table (base for all item types)
├── id, text, position, list_id, item_type
├── created_at, updated_at
└── Indexes: pk, list_id, item_type, position

todo_items table (todo-specific data)
├── id (PK + FK to items.id)
├── 24 todo-specific columns
└── 14 indexes for performance

Key Files Modified

  1. services/domain/primitives.py - Added Item primitive
  2. services/domain/models.py - Todo extends Item
  3. services/database/models.py - ItemDB, TodoDB with polymorphism
  4. services/item_service.py - Universal service (NEW)
  5. services/todo_service.py - Todo service extending ItemService (NEW)
  6. services/repositories/todo_repository.py - Updated for polymorphism
  7. services/api/todo_management.py - Wired services
  8. alembic/versions/40fc95f25017_create_items_table.py - Phase 1 migration
  9. alembic/versions/234aa8ec628c_refactor_todos_to_extend_items.py - Phase 2 migration

Design Decisions (from Phase 3)

  1. ENUM vs String Types: Use String in database (not PostgreSQL ENUMs)
    • Rationale: Flexible, no migrations for new values, matches migration intent
  2. Service Inheritance: TodoService extends ItemService (not composition)
    • Rationale: Clear IS-A relationship, matches domain/database patterns
  3. Dependency Injection: Services created on-demand in FastAPI
    • Rationale: No global state, proper async, easy testing
  4. Repository Access: Services use repositories internally
    • Rationale: Clean separation, business logic in services, data access in repositories

References

Timeline

Future Work

Short Term

Medium Term

Long Term

Authors


ADR-041: Domain Primitives Refactoring Status: Implemented ✅ Last Updated: November 4, 2025 Branch: foundation/item-list-primitives