Virtual Subject Injection: Seamless Integration with Existing UI
Adding a new adaptive curriculum to an existing mobile app typically requires months of UI development: new screens, navigation patterns, progress indicators, and lesson displays. This frontend work can delay feature launches and introduce platform-specific bugs (iOS vs. Android inconsistencies).
We bypassed all of that by creating a virtual subject—an entry in the subject tree that doesn't correspond to static content but instead triggers adaptive lesson generation. The mobile app treats it like any other subject, using existing lesson endpoints and UI components. Result: zero mobile app changes, seamless "Practice" subject integration, and instant deployment.
The Traditional Approach: Rebuild the UI
Adding adaptive curriculum typically follows this path:
- Backend: Build adaptive lesson generation API
- iOS: Create new "Practice" tab with custom UI
- Android: Replicate iOS UI with Android components
- Testing: Test both platforms independently
- Release: Coordinate mobile app releases with backend deployment
This process takes 2-3 months and creates tight coupling between backend features and frontend capabilities.
Before: New UI Required
Static Subjects Mobile UI
┌──────────────────┐ ┌──────────────────┐
│ Subject ID: 1 │───────>│ Play Tab │
│ Subject ID: 2 │ │ - Subject List │
│ Subject ID: 3 │ │ - Lesson Display │
└──────────────────┘ └──────────────────┘
Requires new UI for
adaptive content
The Virtual Subject Approach: Reuse Existing UI
Instead of building new UI, we inject a virtual subject (ID: -6, name: "Practice") into the subject tree API. The mobile app renders it like any other subject, but when the user taps it, the backend dynamically generates an adaptive lesson instead of serving static content.
After: Zero UI Changes
Static + Virtual Mobile UI (Unchanged)
┌──────────────────┐ ┌──────────────────┐
│ Subject ID: 1 │───────>│ Play Tab │
│ Subject ID: 2 │ │ - Subject List │
│ Subject ID: 3 │ │ + "Practice" │
│ Subject ID: -6 │◄───────│ - Same UI │
│ (Virtual/Adapt) │ │ - Zero changes │
└──────────────────┘ └──────────────────┘
The mobile app knows nothing about adaptive curriculum. It sees "Practice" as just another subject with chapters and lessons.
Technical Implementation
Subject Tree API Modification
The subject tree endpoint (/api/v1/subjects/tree) returns a hierarchical list of subjects, chapters, and lessons. We modified it to inject the virtual subject:
def get_subject_tree(user_id, app_name):
# Fetch static subjects from database
subjects = Subject.query.filter_by(app_name=app_name).all()
# Check if adaptive curriculum is enabled for this app
if is_adaptive_enabled(app_name):
# Inject virtual subject
virtual_subject = {
"id": -6,
"name": "Practice",
"description": "Personalized lessons based on your progress",
"icon_url": "https://cdn.alphazed.app/icons/practice.png",
"chapters": [
{
"id": -100,
"name": "Adaptive Lessons",
"lessons": [] # Populated dynamically
}
],
"order": 0 # Appears first in list
}
subjects.insert(0, virtual_subject)
return subjects
The negative ID (-6) distinguishes virtual subjects from real ones (positive IDs). This convention prevents ID collisions and signals special handling.
Virtual Chapter IDs
Each adaptive lesson also gets a virtual chapter ID (-100) to maintain the three-level hierarchy (subject → chapter → lesson) that the mobile app expects:
{
"subject_id": -6,
"chapters": [
{
"id": -100,
"name": "Adaptive Lessons",
"lessons": [
{
"id": 12345,
"title": "Lesson 1",
"items": [...]
}
]
}
]
}
The mobile app renders this identically to static subjects, but the backend knows that subject_id: -6 triggers adaptive generation.
Lesson Endpoint Routing
When the user taps a lesson in the "Practice" subject, the mobile app calls the standard lesson endpoint:
GET /api/v1/lessons/next?subject_id=-6&chapter_id=-100
The backend detects the virtual subject ID and routes to the adaptive lesson generator instead of the static lesson repository:
@app.route('/api/v1/lessons/next')
def get_next_lesson():
subject_id = request.args.get('subject_id', type=int)
chapter_id = request.args.get('chapter_id', type=int)
user_id = get_current_user_id()
# Check if this is a virtual subject
if subject_id == -6:
# Generate adaptive lesson
lesson = content_duo_service.generate_lesson(user_id)
return jsonify(lesson)
else:
# Fetch static lesson
lesson = static_lesson_service.get_next_lesson(subject_id, chapter_id)
return jsonify(lesson)
The mobile app receives a lesson in the same format as static lessons, so existing rendering code works without modification.
Advantages of Virtual Subjects
1. Zero Mobile App Changes
The biggest win: no iOS or Android development required. The backend controls feature rollout entirely through API changes.
2. Instant Deployment
Backend deploys are faster and less risky than mobile releases. We can deploy adaptive curriculum, test it, and iterate multiple times per day—impossible with mobile app stores.
3. Platform Consistency
Since both iOS and Android use the same API, there's no risk of platform-specific bugs or UI inconsistencies. If it works on one platform, it works on both.
4. Gradual Rollout
Feature flags control virtual subject injection per app, per environment, or per user cohort. We can:
- Enable for 10% of users (A/B testing)
- Enable for staging but not production
- Enable for Amal but not Thurayya
5. Fallback Safety
If adaptive lesson generation fails (database error, timeout, etc.), we can gracefully fall back to static content or hide the virtual subject entirely—no mobile app crash.
Implementation Details
Files Modified
src/resources/user/play/subject_tree.py- Subject tree API endpointsrc/resources/user/play/lesson.py- Lesson endpoint routingsrc/services/content_duo/content_duo.py- Adaptive lesson generation
Commits
831e7c4- Add virtual subject injection to subject tree APIf0a0917- Route lesson requests to adaptive generator22d4e61- Add feature flag for virtual subject enablementf0ac780- Add error handling and fallback logic
Test Coverage
15 integration tests covering:
- Virtual subject appears in tree when enabled
- Virtual subject absent when disabled
- Lesson routing to adaptive generator
- Fallback to static content on error
- Per-app feature flag behavior
Backward Compatibility
Older mobile app versions that don't recognize virtual subjects handle them gracefully:
// Mobile app code (simplified)
subjects.forEach(subject => {
if (subject.id > 0) { // Skip non-positive IDs
renderSubject(subject);
}
});
This filtering logic (present in older versions) hides virtual subjects from users on outdated apps, preventing confusion until they update.
Virtual Subject Metadata
The virtual subject includes metadata that helps the mobile app render it appropriately:
{
"id": -6,
"name": "Practice",
"description": "Personalized lessons based on your progress",
"icon_url": "https://cdn.alphazed.app/icons/practice.png",
"is_adaptive": true,
"estimated_time_minutes": 5,
"difficulty_level": "adaptive",
"completion_percentage": 0 // Adaptive subjects don't have fixed completion
}
The is_adaptive: true flag allows newer mobile apps to show special UI (e.g., animated icons, "Personalized for you" badge) while older apps simply render it as a normal subject.
Handling Edge Cases
Empty Lesson Pools
If the user has completed all available content, generate_lesson() returns null. The backend removes the virtual subject from the tree:
lesson = content_duo_service.generate_lesson(user_id)
if lesson is None:
# User has completed all content, hide virtual subject
return subjects # Without virtual subject injected
First-Time Users
New users have no attempt history, so HLR can't calculate retention scores. The adaptive generator defaults to beginner-level content with standard slot distribution until 10+ attempts have been recorded.
Concurrent Lesson Requests
If a user taps "Practice" repeatedly, we prevent duplicate session creation with idempotency keys:
def generate_lesson(user_id, idempotency_key=None):
# Check for existing session with this key
if idempotency_key:
existing = Session.query.filter_by(
user_id=user_id,
idempotency_key=idempotency_key
).first()
if existing:
return existing.lesson
# Generate new lesson
...
Performance Considerations
Virtual subject injection adds minimal latency to the subject tree API (<5ms). The adaptive lesson generation (triggered on lesson request, not subject tree load) takes 50-150ms depending on user history size.
To optimize:
- Cache user persona calculations (5-minute TTL)
- Pre-calculate retention scores for frequently-accessed concepts
- Index
last_attempt_atanduser_idfor fast queries
Future Enhancements
Multiple Virtual Subjects
We could inject multiple virtual subjects for different purposes:
- "Daily Challenge" (high-difficulty adaptive lessons)
- "Quick Review" (review-only lessons, no new content)
- "Vocabulary Builder" (vocabulary-focused adaptive lessons)
Each would have a different negative ID and configuration.
Dynamic Subject Names
Personalize the virtual subject name based on user persona:
- Beginner: "Your Learning Path"
- Intermediate: "Practice & Review"
- Advanced: "Master Challenge"
Progress Indicators
While adaptive subjects don't have fixed completion percentages, we can show relative progress:
- Concepts mastered: 45 / 200
- Current mastery level: Intermediate
- Study streak: 7 days
Results: Seamless Integration
Virtual subject injection delivered:
- Zero mobile app changes - Both iOS and Android work without modification
- Instant deployment - Backend-only feature rollout
- Seamless UX - "Practice" subject appears naturally in Play tab
- Platform consistency - Identical behavior on iOS and Android
- Future-proof architecture - Backend experiments don't require frontend coordination
Key Technical Insight
Virtual IDs (negative integers) provide a clean separation between static and dynamic content. This pattern generalizes beyond adaptive curriculum:
- Virtual achievements (dynamic based on behavior)
- Virtual leaderboards (real-time ranking)
- Virtual challenges (time-limited events)
Any dynamic content can be injected as virtual entities, allowing backend experimentation without frontend coupling.
Implementation Files: src/resources/user/play/, src/services/content_duo/
Commits: 831e7c4, f0a0917, 22d4e61, f0ac780
Strategy: Inject virtual subject ID=-6 into subject tree API