PRD: Hot Video Priority & Trending Feed¶
Overview¶
Status: Approved Author: nguyenhuuca Date: 2026-02-28 Version: 1.0 Beads Issue: N/A PR-FAQ: N/A Stakeholders: Backend Team, Frontend Team, Product
Problem Statement¶
All videos are displayed in a flat, unordered list. Users have no signal about which videos are trending or popular — leading to poor content discovery and lower engagement. Users who want to watch something good must manually browse without any curation.
Evidence¶
Quantitative Evidence:
- No trending signal exists — 100% of content discovery is manual browsing
- VideoAccessStats table exists and tracks hitCount + lastAccessedAt, but recordAccess() is commented out — zero access data is being written
- ShareLink table tracks share events but is not used for ranking
- VideoCacheImpl already promotes videos with ≥5 cache hits — evidence that popularity signals are valuable
Qualitative Evidence:
- Users who want to watch "something good" have no starting point other than scrolling
- Hot content buried in a flat list reduces the chance of viral spread
- Pre-caching popular videos is already partially designed in VideoCacheImpl — the infrastructure anticipates this need
Goals & Success Metrics¶
| Goal | Metric | Target |
|---|---|---|
| Improve content discovery | Average session duration | +15% |
| Increase video consumption | Videos watched per session | +20% |
| Reduce hot video latency | Time-to-first-frame for hot videos | < 500ms (from ~2s via pre-cache) |
| Drive trending engagement | User click-through on hot badges | > 30% |
User Stories¶
Viewer¶
- As a viewer, I want to see a "Trending Now" section at the top of the home page so that I can quickly find popular content without browsing.
- Acceptance: A "🔥 Trending Now" section shows top 5 hot videos above the main feed.
- As a viewer, I want hot videos to be visually marked so that I know at a glance which content is currently popular.
- Acceptance: Video cards with
isHot = truedisplay a 🔥 badge. - As a viewer, I want hot videos to start playing faster so that I don't wait when choosing trending content.
- Acceptance: Top 10 hot videos are pre-warmed in cache; first-frame latency < 500ms.
Content Viewer (API consumer)¶
- As an API consumer, I want a dedicated
/api/videos/trendingendpoint so that I can build trending feeds in any client. - Acceptance:
GET /api/videos/trending?limit=Nreturns top N videos sorted byhotScore DESC. - As an API consumer, I want to sort the main video list by hotness so that I can offer multiple sort options without a separate endpoint.
- Acceptance:
GET /api/videos?sort=hotreturns all videos sorted byhotScore DESC; existing calls withoutsortare unchanged.
Requirements¶
Functional Requirements¶
| ID | Requirement | Priority | Notes |
|---|---|---|---|
| FR-1 | Activate recordAccess() — async upsert of VideoAccessStats on every stream |
Must Have | Must not block stream response |
| FR-2 | Implement hot score formula: weighted hits (60%) + shares (25%) + recency decay (15%) | Must Have | Min-max normalised across active videos |
| FR-3 | GET /api/videos/trending?limit=N endpoint (default 10, max 50) |
Must Have | Served from StatsCacheImpl, TTL 30 min |
| FR-4 | Extend GET /api/videos?sort=hot — backward compatible |
Must Have | No change to calls without sort param |
| FR-5 | Scheduled job: recompute hot scores every 30 minutes | Must Have | Runs in AppScheduler |
| FR-6 | Pre-warm VideoCacheImpl for top 10 hot videos after each recomputation |
Should Have | Goal: first-frame < 500ms |
| FR-7 | Frontend: "🔥 Trending Now" section at top of home page (top 5 videos) | Must Have | |
| FR-8 | Frontend: "🔥 Hot" badge on video cards where isHot = true |
Must Have | Show when hotScore ≥ top 20% threshold |
Non-Functional Requirements¶
| ID | Requirement | Target |
|---|---|---|
| NFR-1 | recordAccess() latency overhead |
< 5ms (async, fire-and-forget) |
| NFR-2 | Trending API response time | < 100ms (served from cache) |
| NFR-3 | Score recomputation time | < 10s for 10,000 videos |
| NFR-4 | Additional memory overhead | < 50MB |
| NFR-5 | Backward compatibility | GET /api/videos without sort param — unchanged behaviour |
| NFR-6 | Resilience | DB write failure in recordAccess() must not fail the stream request |
Scope¶
In Scope¶
- Activate
VideoAccessServiceImpl.recordAccess()(upsertVideoAccessStats) - Hot score algorithm with configurable weights (
app.hot-video.*inapplication.yaml) - New
GET /api/videos/trendingendpoint sort=hotparameter on existingGET /api/videos- Liquibase migration:
hot_score,is_hot,hot_rankcolumns onvideo_sources - Scheduled recomputation job every 30 minutes
- Cache pre-warming for top 10 hot videos
- Frontend: Trending Now section + 🔥 badge on video cards
Out of Scope¶
- Personalised recommendations (per-user hot scores) — v2
- Real-time score updates via WebSocket — v2
- A/B testing framework for ranking algorithms — v2
- Machine learning-based scoring — v3
- Like/dislike counts (not present on
VideoSourceentity)
Dependencies¶
| Dependency | Owner | Status | Risk |
|---|---|---|---|
VideoAccessStats entity + repo |
Backend | ✅ Exists | Low — just needs recordAccess() uncommented |
ShareLink table |
Backend | ✅ Exists | Low — read-only query for share count |
StatsCacheImpl |
Backend | ✅ Exists | Low — already used for stats |
VideoCacheImpl |
Backend | ✅ Exists | Low — preWarm() needs to be called |
AppScheduler |
Backend | ✅ Exists | Low — add new @Scheduled method |
| Liquibase migration | Backend | ❌ Missing | Medium — must land before entity changes |
VideoSource entity |
Backend | ✅ Active table | Low — add 3 columns |
Risks & Mitigations¶
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
recordAccess() creates DB bottleneck under high load |
Medium | High | Keep @Async + catch all exceptions; add write queue in v2 if needed |
| Hot score biased toward old high-hit videos | Medium | Medium | Apply 24h rolling window + exponential recency decay (e^(-0.1 × hoursAgo)) |
video_access_stats table grows unbounded |
Medium | Medium | Activate cleanUpOldVideos() in AppScheduler to purge rows older than window |
| Scores stale up to 30 minutes after new viral video | Low | Low | TTL is configurable; acceptable for v1 |
| Cache invalidation on new video added | Low | Low | TTL-based invalidation (30 min) sufficient for v1 |
Open Questions¶
- [ ] Should
/api/videos/trendingbe a public endpoint (no auth required)? — Owner: Backend — Due: Before FR-3 implementation - [ ] What threshold defines
isHot— top 20% by score or a fixed score cutoff? — Owner: Product — Due: Before FR-2 implementation - [ ] Should shares from private share-links count toward the hot score? — Owner: Backend — Due: Before FR-2 implementation
- [ ] Do we need a
hot_score_historytable for analytics / score trending over time? — Owner: Data — Due: v2 planning
Appendix¶
Data Model¶
-- Liquibase migration: 202602280001-add-hot-score.sql
ALTER TABLE video_sources ADD COLUMN hot_score DECIMAL(5,4) DEFAULT 0;
ALTER TABLE video_sources ADD COLUMN is_hot BOOLEAN DEFAULT FALSE;
ALTER TABLE video_sources ADD COLUMN hot_rank INT DEFAULT NULL;
CREATE INDEX idx_video_sources_hot_score
ON video_sources(hot_score DESC) WHERE is_hot = TRUE;
CREATE INDEX IF NOT EXISTS idx_video_access_stats_video_id
ON video_access_stats(video_id);
Hot Score Formula¶
hotScore = (0.60 × normalizedHits) + (0.25 × normalizedShares) + (0.15 × recencyBoost)
recencyBoost = e^(-0.1 × hoursAgo)
All inputs min-max normalised to [0, 1] across active videos.
API Contract¶
GET /api/videos/trending
Query params: limit (int, default=10, max=50)
Response: ResultListInfo<VideoDto>
data[].videoId String
data[].title String
data[].embedLink String
data[].hotScore Double
data[].isHot Boolean
data[].rank Integer (1 = hottest)
GET /api/videos?sort=hot
New Components¶
service/
├── HotVideoService.java
└── impl/HotVideoServiceImpl.java
jobs/
└── AppScheduler.java ← add recomputeHotScores()
Research¶
VideoAccessStatsentity:api/src/main/java/com/canhlabs/funnyapp/entity/VideoAccessStats.javaVideoSourceentity:api/src/main/java/com/canhlabs/funnyapp/entity/VideoSource.javaVideoCacheImpl:api/src/main/java/com/canhlabs/funnyapp/cache/
Approval¶
| Role | Name | Date | Status |
|---|---|---|---|
| Product | nguyenhuuca | 2026-02-28 | Approved |
| Engineering | nguyenhuuca | 2026-02-28 | Approved |
| Design | N/A | N/A | N/A |
Next Steps & Handoffs¶
After PRD approval:
- [x] Architect Review: Technical feasibility assessment
-
Output: ADR-0011
-
[ ] UI/UX Designer: Wireframes for Trending Now section + Hot badge
- Trigger:
/ui-ux-designer -
Output: Design Spec (
artifacts/design_spec_hot-video.md) -
[x] Engineering Estimate: Effort estimation and decomposition
-
Output: plan-hot-video-priority
-
[ ] Create Beads Issues: Decompose into trackable work items
- Command:
bd create "Hot Video Priority" -t feature
Related Artifacts: - ADR: ADR-0011 — Hot Video Priority Scoring - Implementation Plan: plan-hot-video-priority - Cache Strategy: ADR-0003 - LRU Cache: ADR-0005
Version History¶
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-02-28 | nguyenhuuca | Initial draft |
| 1.1 | 2026-05-31 | nguyenhuuca | Restructured to official PRD template; removed non-existent upCount field; fixed entity name YouTubeVideo → VideoSource; fixed doc paths |