I Overview
A trending signal that surfaces popular videos and pre-warms them for faster playback.
๐ฏ What & Why
This PRD introduces a hot-score ranking, a dedicated trending endpoint, a sort option on the existing feed, a scheduled recompute job, cache pre-warming for hot videos, and frontend surfacing (trending section + ๐ฅ badge).
๐ Metadata
| Field | Value |
|---|---|
| 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 |
๐ Next Steps & Related Artifacts
After PRD approval
- [x] Architect Review: Technical feasibility โ Output: ADR-0011
- [ ] UI/UX Designer: Wireframes for Trending Now section + Hot badge (
/ui-ux-designer) โ Output:artifacts/design_spec_hot-video.md - [x] Engineering Estimate: Effort estimation & decomposition โ Output: plan-hot-video-priority
- [ ] Create Beads Issues:
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
Approval
| Role | Name | Date | Status |
|---|---|---|---|
| Product | nguyenhuuca | 2026-02-28 | Approved |
| Engineering | nguyenhuuca | 2026-02-28 | Approved |
| Design | N/A | N/A | N/A |
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 |
II Problem Statement
No trending signal exists โ discovery is 100% manual browsing.
โ The Problem
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.
๐ Quantitative Evidence
- No trending signal exists โ 100% of content discovery is manual browsing
VideoAccessStatstable exists and trackshitCount+lastAccessedAt, butrecordAccess()is commented out โ zero access data is being writtenShareLinktable tracks share events but is not used for rankingVideoCacheImplalready 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
III Goals & Success Metrics
Discovery, consumption, latency, and trending engagement.
๐ Goals & Targets
| 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% |
IV User Stories
Viewer-facing experience and API consumer integration.
Viewer Trending Now section
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.
Viewer Visual hot marker
As a viewer, I want hot videos to be visually marked so that I know at a glance which content is currently popular.
isHot = true display a ๐ฅ badge.Viewer Faster hot playback
As a viewer, I want hot videos to start playing faster so that I don't wait when choosing trending content.
API consumer Dedicated trending endpoint
As an API consumer, I want a dedicated /api/videos/trending endpoint so that I can build trending feeds in any client.
GET /api/videos/trending?limit=N returns top N videos sorted by hotScore DESC.API consumer Sort main list by hotness
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.
GET /api/videos?sort=hot returns all videos sorted by hotScore DESC; existing calls without sort are unchanged.V Requirements
Functional (FR) and non-functional (NFR) 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 |
VI Scope
What ships in v1 versus what is deferred.
โ 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)
VII Dependencies
Most infrastructure already exists; the Liquibase migration is the one gap.
๐ 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 |
VIII Risks & Mitigations
Load, score bias, table growth, and staleness โ each with a mitigation.
โ ๏ธ 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 |
IX Data & API
Appendix โ data model, hot score formula, API contract, and new components.
๐๏ธ 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
๐ 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
Same VideoDto structure, ordered by hotScore DESC
๐งฉ New Components
service/
โโโ HotVideoService.java
โโโ impl/HotVideoServiceImpl.java
jobs/
โโโ AppScheduler.java โ add recomputeHotScores()
๐ Research โ existing code references
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/
X Open Questions
Decisions to resolve before the related requirements are implemented.
๐งฉ Open Questions
/api/videos/trending be a public endpoint (no auth required)? โ Owner: Backend โ Due: Before FR-3 implementationisHot โ top 20% by score or a fixed score cutoff? โ Owner: Product โ Due: Before FR-2 implementationhot_score_history table for analytics / score trending over time? โ Owner: Data โ Due: v2 planning