Close
Youtube history feature Low level design

Low Level Design of Youtube’s last watched video feature (part – 2)

Introduction

In the previous part we discussed how we can efficiently update today’s watched video history and maintain it. Now lets build on that idea further and discuss the design of the history feature and other subsystems with limit scope, I included those for completeness. So let’s dive in.

Problem defination, requirement and scope

Lets try to define the problem and requirements before we discuss this further.

  • Users should see the last watched video at the top of their watched history.
  • Users should watch the actual video for at least 1 sec to consider as watched.
  • Users watched the ads that just played, and if the user exited the video before the actual video starts. Then will not be considered as watched.
  • Users should be able to see the last watched video history for the last 30 days and history stored per day is capped at 100 videos.
  • Video type would be .mp4 with 1080p. We can extend if needed, for illustration I will take only one video type/Video quality.

Below subsystems features discussed with limited scope for completeness

  • Users should be able to scroll through the list of available videos and can select one to be played.
  • Users should be able to search with query for a desired videos and scroll through the list.

Design consideration and class diagram interaction

Looking at the requirements they are like user stories, each one of them hints some details. What we need to do here is to find out the objects and actions. In the above requirement I see below objects/classes that would be needed.

  • User ( Stores all info about users)
  • Video (Stores details of video)
  • Video Searching (Search based on user search query)
  • Video Library ( select and scroll through video)
  • Video player (plays the select video)
  • WatchedHistory (Stores today watched history)
  • DataBaseClient (To get history data older than today from appserver)
  • App controller Cordinator between all the subsystems)

One level above:

  • Video Recommendation System Client(Get the list of videos recommended from the Recommendation system reside on App server – this out of our scope in this discussion)

Implementation

Now lets see how to implement this, below I showed the class diagram, which kind of shows class interactions and their supported API’s. If you see there are many subsystems here and it is kind of complex interactions, but they are kind of self explanatory.

The only thing I want to explain about is How does the WatchedHistory class know if video has to be added to the history list? The way I have implemented here is to use the Observer design pattern, Where WatchedHistory class added as a status listener in VideoPlayer class. Which make sure when the actual video is being played then only notify the WatchedHistory class to update its history, Otherwise not.

Low level design of youtube's watched history feature

Also I have pasted all the code for the below subsystems for trying it in your editor.

YouTubeAppController.java

package working.example.tech.LLD.YouTubeVideoHistory;
import working.example.tech.interfaces.TestCaseRunner;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class YoutubeAppController implements TestCaseRunner {
    private VideoRecommendationSystemClient vRecSys;
    private SearchQuery searchQuery;
    private WatchedHistory wHis;
    private VideoLibrary videoLib;
    private VideoPlayer videoPlayer;
    private  User user;
    public YoutubeAppController() {
        this.vRecSys = new VideoRecommendationSystemClient();
        this.searchQuery = new SearchQuery();
        this.wHis = new WatchedHistory();
        this.videoLib = new VideoLibrary();
        this.videoPlayer = new VideoPlayer();
        this.user = new User("1234", "WorkingExample", "");
    }
    public void RunTest() {
        System.out.println("Opening the Youtube APP");
        videoPlayer.addSubscriber(wHis.getListener());
        videoLib.setSearchResultToScroll(searchQuery.getSearchResult("Default"));
        for (int current = 0 ; current < 2; current++) {
            videoLib.scroll(current);
            videoPlayer.play(videoLib.select(current, user), user);
        }
        System.out.println("Get Watched history for user: " + user.toString());
        Map<Integer, List<Video>> his = wHis.getHistory(user, 2);
        for(Map.Entry<Integer, List<Video>> entry : his.entrySet()) {
            System.out.println("History for today - " + entry.getKey());
            for(Video in : entry.getValue()) {
                System.out.println(in);
            }
        }
    }
    public void showOut() {}
}

WatchedHistory.java

package working.example.tech.LLD.YouTubeVideoHistory;
import working.example.tech.LastWatchedYoutubeVideo.WatchedVideoDayHistory;
import java.util.List;
import java.util.Map;
import java.util.Vector;
public class WatchedHistory {
     WatchedVideoDayHistory wHisMantain;
     Integer maxStorge;
     VideoStartedListener listener;
     DataBaseClient db;
     public VideoStartedListener getListener() {
        return listener;
     }
    public WatchedHistory() {
        this.wHisMantain = new WatchedVideoDayHistory();
        this.maxStorge = 100;
        this.listener = new VideoStartedListener();
        db = new DataBaseClient();
    }
    class VideoStartedListener implements VideoStartedStatusListener {
        @Override
        public void notify(User user, Video video) {
               updateHistory(user, video);
        }
        @Override
        public String getListenerName() {
            return WatchedHistory.class.toString() + "Listener";
        }
    }
    private void updateHistory(User user, Video video) {
        Vector<String> vec = new Vector<>();
        System.out.println("Adding current video to today History: " + video.toString());
        vec.add(video.getVideoId());
        this.wHisMantain.run(vec, maxStorge);
    }
    public Map<Integer, List<Video>>  getHistory(User user, int lastNDays) {
        return db.getLastWatchedVideo(user, lastNDays);
    }
}

VideoStartedStatusListener.java

package working.example.tech.LLD.YouTubeVideoHistory;
public interface VideoStartedStatusListener {
    void notify(User user, Video video);
    String getListenerName();
}

VideoRecommendationSystemClient.java

package working.example.tech.LLD.YouTubeVideoHistory;
import java.util.ArrayList;
import java.util.List;
public class VideoRecommendationSystemClient {
    List<Video> getRecommendedVideosForUser(User user) {
        // here we can contact the server to get the recommendation
        // returning few dummy values for illustration
        List<Video> recomm = new ArrayList<>();
        recomm.add(new Video("123",
                "mp4", "5","12-1-23","a"));
        recomm.add(new Video("456",
                "mp4", "15","12-2-23","b"));
        recomm.add(new Video("567",
                "mp4", "25","12-3-23","c"));
        recomm.add(new Video("890",
                "mp4", "45","12-4-23","d"));
        recomm.add(new Video("345",
                "mp4", "25","12-5-23","e"));
        recomm.add(new Video("453",
                "mp4", "95","12-6-23","f"));
        recomm.add(new Video("346",
                "mp4", "75","12-7-23","g"));
        return recomm;
    }
}

VideoPlayer.java

package working.example.tech.LLD.YouTubeVideoHistory;
import java.util.ArrayList;
import java.util.List;
public class VideoPlayer {
    List<VideoStartedStatusListener> listeners;
    boolean exited;
    public VideoPlayer() {
        listeners = new ArrayList<>();
        exited = false;
    }
    public void addSubscriber(VideoStartedStatusListener listener) {
        listeners.add(listener);
    }
    public void play(Video video, User user) {
        System.out.println("Now playing Ads");
        synchronized (this) {
            if (exited) {
                // setting the exit status for next video to false
                exited = false;
                return;
            }
        }
        for(VideoStartedStatusListener listener : listeners) {
            System.out.println("Notifying video Started to: " + listener.getListenerName());
            listener.notify(user,video);
        }
        System.out.println("Now playing Video: " + video.getVideoId() + " for: " +
                video.getVideoLenInSec() + "sec");
        System.out.println("Exiting");
    }
    public void pause(Video video) {
        System.out.println("Paused Video" + video.getVideoId());
    }
    public void exit(Video video) {
        synchronized (this) {
            this.exited = true;
        }
        System.out.println("Force exiting Video" + video.getVideoId());
    }
}

VideoLibrary.java

package working.example.tech.LLD.YouTubeVideoHistory;
import java.util.ArrayList;
import java.util.List;
public class VideoLibrary {
    List<Video> searchResultToScroll;
    public void setSearchResultToScroll(List<Video> searchResultToScroll) {
        this.searchResultToScroll = searchResultToScroll;
    }
    public int scroll(int CurrentPos) {
        //Single call to this method would help scroll through screen next 2 video
        CurrentPos += 2;
        System.out.println("Scrolling through... now at video: " + CurrentPos);
        return CurrentPos;
    }
    public Video select(int posOfSelectedVideo, User user) {
        Video out = searchResultToScroll.get(posOfSelectedVideo);
        System.out.println("selected video details:" + out.toString());
        return out;
    }
}

Video.java

package working.example.tech.LLD.YouTubeVideoHistory;
public class Video {
    private String videoId;
    private String videoFormat;
    private String VideoLenInSec;
    private String VideoFirstPublishedDate;
    private String ChannelName;
    public Video(String videoId, String videoFormat,
                 String videoLenInSec, String videoFirstPublishedDate, String channelName) {
        this.videoId = videoId;
        this.videoFormat = videoFormat;
        VideoLenInSec = videoLenInSec;
        VideoFirstPublishedDate = videoFirstPublishedDate;
        ChannelName = channelName;
    }
    public String getVideoId() {
        return videoId;
    }
    public String getVideoFormat() {
        return videoFormat;
    }
    public String getVideoLenInSec() {
        return VideoLenInSec;
    }
    public String getVideoFirstPublishedDate() {
        return VideoFirstPublishedDate;
    }
    public String getChannelName() {
        return ChannelName;
    }
    @Override
    public String toString() {
        return "Video{" +
                "videoId='" + videoId + '\'' +
                ", videoFormat='" + videoFormat + '\'' +
                ", VideoLenInSec='" + VideoLenInSec + '\'' +
                ", VideoFirstPublishedDate='" + VideoFirstPublishedDate + '\'' +
                ", ChannelName='" + ChannelName + '\'' +
                '}';
    }
}

User.java

package working.example.tech.LLD.YouTubeVideoHistory;
public class User {
    private String UseId;
    private String userName;
    private String Address;
    /*We can add more details of user as needed*/
    public User(String useId, String userName, String address) {
        UseId = useId;
        this.userName = userName;
        Address = address;
    }
    public String getUseId() {
        return UseId;
    }
    public String getUserName() {
        return userName;
    }
    public String getAddress() {
        return Address;
    }
    @Override
    public String toString() {
        return "User{" +
                "UseId='" + UseId + '\'' +
                ", userName='" + userName + '\'' +
                ", Address='" + Address + '\'' +
                '}';
    }
}

SearchQuery.java

package working.example.tech.LLD.YouTubeVideoHistory;
import java.util.ArrayList;
import java.util.List;
public class SearchQuery {
    List<Video> searchResult;
    public List<Video> getSearchResult(String query) {
        // This method would connect to the Server DB to fetch the result of the
        // query
        searchResult = new ArrayList<>();
        searchResult.add(new Video("123",
                "mp4", "5","12-1-23","a"));
        searchResult.add(new Video("456",
                "mp4", "15","12-2-23","b"));
        searchResult.add(new Video("567",
                "mp4", "25","12-3-23","c"));
        searchResult.add(new Video("890",
                "mp4", "45","12-4-23","d"));
        searchResult.add(new Video("345",
                "mp4", "25","12-5-23","e"));
        searchResult.add(new Video("453",
                "mp4", "95","12-6-23","f"));
        searchResult.add(new Video("346",
                "mp4", "75","12-7-23","g"));
        return searchResult;
    }
}

DataBaseClient.java

package working.example.tech.LLD.YouTubeVideoHistory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DataBaseClient {
    public Map<Integer, List<Video>> getLastWatchedVideo(User user, int noOfDays) {
        //here it connects to the server DB to get the info.
        //returning dummy values just to demonstrate
        Map<Integer, List<Video>> lastNdaysVideos = new HashMap<>();
        List<Video> one = new ArrayList<>();
        List<Video> two = new ArrayList<>();
        one.add(new Video("123",
                "mp4", "5","12-1-23","a"));
        one.add(new Video("598",
                "mp4", "35","12-5-23","b"));
        one.add(new Video("098",
                "mp4", "55","12-7-23","c"));
        one.add(new Video("124",
                "mp4", "52","12-6-23","d"));
        two.add(new Video("067",
                "mp4", "59","12-9-23","e"));
        two.add(new Video("354",
                "mp4", "53","12-2-23","f"));
        two.add(new Video("678",
                "mp4", "51","12-1-23","g"));
        two.add(new Video("989",
                "mp4", "50","12-3-23","a"));
        lastNdaysVideos.put(1, one);
        lastNdaysVideos.put(2, two);
        return lastNdaysVideos;
    }
}

Main.java

import working.example.tech.LastWatchedYoutubeVideo.
        WatchedVideoDayHistory;
import working.example.tech.interfaces.TestCaseRunner;
public class Main {
    public static void main(String[] args) {
        TestCaseRunner youTubeContoller = new YoutubeAppController();
        youTubeContoller.RunTest();
    }
}

Output

Opening the Youtube APP
Scrolling through... now at video: 2
selected video details:Video{videoId='123', videoFormat='mp4', VideoLenInSec='5', VideoFirstPublishedDate='12-1-23', ChannelName='a'}
Now playing Ads
Notifying video Started to: class working.example.tech.LLD.YouTubeVideoHistory.WatchedHistoryListener
Adding current video to today History: Video{videoId='123', videoFormat='mp4', VideoLenInSec='5', VideoFirstPublishedDate='12-1-23', ChannelName='a'}
Today's Last Watched History: 123
Now playing Video: 123 for: 5sec
Exiting
Scrolling through... now at video: 3
selected video details:Video{videoId='456', videoFormat='mp4', VideoLenInSec='15', VideoFirstPublishedDate='12-2-23', ChannelName='b'}
Now playing Ads
Notifying video Started to: class working.example.tech.LLD.YouTubeVideoHistory.WatchedHistoryListener
Adding current video to today History: Video{videoId='456', videoFormat='mp4', VideoLenInSec='15', VideoFirstPublishedDate='12-2-23', ChannelName='b'}
Today's Last Watched History: 456 123
Now playing Video: 456 for: 15sec
Exiting
Get Watched history for user: User{UseId='1234', userName='WorkingExample', Address=''}
History for today - 1
Video{videoId='123', videoFormat='mp4', VideoLenInSec='5', VideoFirstPublishedDate='12-1-23', ChannelName='a'}
Video{videoId='598', videoFormat='mp4', VideoLenInSec='35', VideoFirstPublishedDate='12-5-23', ChannelName='b'}
Video{videoId='098', videoFormat='mp4', VideoLenInSec='55', VideoFirstPublishedDate='12-7-23', ChannelName='c'}
Video{videoId='124', videoFormat='mp4', VideoLenInSec='52', VideoFirstPublishedDate='12-6-23', ChannelName='d'}
History for today - 2
Video{videoId='067', videoFormat='mp4', VideoLenInSec='59', VideoFirstPublishedDate='12-9-23', ChannelName='e'}
Video{videoId='354', videoFormat='mp4', VideoLenInSec='53', VideoFirstPublishedDate='12-2-23', ChannelName='f'}
Video{videoId='678', videoFormat='mp4', VideoLenInSec='51', VideoFirstPublishedDate='12-1-23', ChannelName='g'}
Video{videoId='989', videoFormat='mp4', VideoLenInSec='50', VideoFirstPublishedDate='12-3-23', ChannelName='a'}

Conclusion

In this LLD we discussed youtube last watched video feature, and I also tried to incorporate other systems with very limited scope for completeness. There are many things to ponder about after seeing this, how to handle concurrently, to make the system Asyc. How to handle Client Db and server DB communications & many others, all this was out of scope in this illustration otherwise this blog will become too big. I hope you will be able to figure out those aspects, I will try to see if I can give one more part focusing on those. But for now try to run the code in your editor and enjoy!