DEV Community

ahmet gedik
ahmet gedik

Posted on

Implementing Dynamic Video Categories with Caching

Video platforms need dynamic category systems that adapt to regional content. Here's how I built the category engine for TrendVidStream, a platform serving 8 diverse global regions.

The Challenge

Categories seem simple until you serve multiple regions. Music is universal, but category IDs vary between regions. Some regions have categories that others don't. Category popularity differs dramatically: Gaming dominates in the US, Music leads in UAE.

Data Model

CREATE TABLE categories (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    slug TEXT NOT NULL UNIQUE,
    video_count INTEGER DEFAULT 0,
    updated_at TEXT DEFAULT (datetime('now'))
);

CREATE TABLE category_regions (
    category_id INTEGER NOT NULL,
    region TEXT NOT NULL,
    video_count INTEGER DEFAULT 0,
    rank INTEGER DEFAULT 0,
    PRIMARY KEY (category_id, region),
    FOREIGN KEY (category_id) REFERENCES categories(id)
);
Enter fullscreen mode Exit fullscreen mode

Category Manager

<?php

class CategoryManager
{
    private Cache $cache;
    private PDO $db;

    public function __construct(PDO $db, Cache $cache)
    {
        $this->db = $db;
        $this->cache = $cache;
    }

    public function getAll(): array
    {
        $cached = $this->cache->get('global:categories');
        if ($cached !== null) {
            return $cached;
        }

        $stmt = $this->db->query('
            SELECT c.*, COUNT(DISTINCT cr.region) as region_count
            FROM categories c
            LEFT JOIN category_regions cr ON c.id = cr.category_id
            GROUP BY c.id
            ORDER BY c.video_count DESC
        ');
        $categories = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $this->cache->set('global:categories', $categories, Cache::TTL_CATEGORIES);
        return $categories;
    }

    public function getForRegion(string $region): array
    {
        $cacheKey = "categories:$region";
        $cached = $this->cache->get($cacheKey);
        if ($cached !== null) {
            return $cached;
        }

        $stmt = $this->db->prepare('
            SELECT c.*, cr.video_count as regional_count, cr.rank
            FROM categories c
            JOIN category_regions cr ON c.id = cr.category_id
            WHERE cr.region = ?
            ORDER BY cr.rank ASC
        ');
        $stmt->execute([$region]);
        $categories = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $this->cache->set($cacheKey, $categories, Cache::TTL_CATEGORIES);
        return $categories;
    }

    public function updateCounts(): void
    {
        $this->db->exec('
            UPDATE categories SET video_count = (
                SELECT COUNT(*) FROM videos WHERE videos.category_id = categories.id
            )
        ');

        $this->db->exec('
            UPDATE category_regions SET video_count = (
                SELECT COUNT(*) FROM videos
                WHERE videos.category_id = category_regions.category_id
                AND videos.region = category_regions.region
            )
        ');

        // Invalidate cache
        $this->cache->delete('global:categories');
    }
}
Enter fullscreen mode Exit fullscreen mode

Cache Invalidation

Categories change rarely, so we use a long TTL (24 hours). Cache is invalidated when:

  1. New videos are fetched (cron updates category counts)
  2. Admin manually saves changes
// In the fetch cron job:
$fetcher->run();
$categoryManager->updateCounts();
// Cache automatically invalidated in updateCounts()
Enter fullscreen mode Exit fullscreen mode

HTTP Cache Headers

<?php

function setCategoryHeaders(): void
{
    header('Cache-Control: public, max-age=10800, stale-while-revalidate=7200');
    header('Vary: Accept-Encoding');
}
Enter fullscreen mode Exit fullscreen mode

The 3-hour max-age with 2-hour stale-while-revalidate means users get fast responses while the cache refreshes in the background.

Category Normalization Across Regions

<?php

class CategoryNormalizer
{
    // YouTube category IDs that map to our unified categories
    private const MAPPING = [
        1 => 'film-animation',
        2 => 'autos-vehicles',
        10 => 'music',
        15 => 'pets-animals',
        17 => 'sports',
        20 => 'gaming',
        22 => 'people-blogs',
        23 => 'comedy',
        24 => 'entertainment',
        25 => 'news-politics',
        26 => 'howto-style',
        27 => 'education',
        28 => 'science-tech',
    ];

    public function normalize(int $youtubeCategoryId): ?string
    {
        return self::MAPPING[$youtubeCategoryId] ?? null;
    }
}
Enter fullscreen mode Exit fullscreen mode

This category system powers the navigation at TrendVidStream, where users can browse trending videos by category across all 8 regions. The caching ensures fast performance while keeping data fresh.

See the full category system in action at trendvidstream.com.

Top comments (0)