Feeds, Notifications & CMS
Three services that all answer one question — "what should this user see, and when?" A recommendation feed the user pulls , notifications the backend pushes , and a content management system whose article-author-tag-category web finally makes graph databases the obvious choice. All three are wired together by the same change-data-capture-into-Kafka backbone.
Assumes you know
After this you should be able to
- Explain the core idea in Feeds, Notifications & CMS
- Apply Feeds, Notifications & CMS in a representative design
Three services that all answer one question — "what should this user see, and when?" A recommendation feed the user pulls, notifications the backend pushes, and a content management system whose article-author-tag-category web finally makes graph databases the obvious choice. All three are wired together by the same change-data-capture-into-Kafka backbone.
This page merges three small course sections that share a spine. Feed
and Notifications
are near-twins — both react to a new row in VideoMetadata, both fan it out to subscribers — the difference is pull vs. push. CMS
is a standalone argument that builds, step by step, to why a graph database
beats both relational and document stores for deeply connected content. Anything marked [+]
is context I added; everything else is rewritten from the seven teaching lessons.
Part A · Video / News Feed Service
The feed starts with Change Data Capture#
The VOD home page pulls its feed from an API. That feed has two sources: (1) videos from channels the user follows, and (2) videos matched to the user's watch behavior (plus trending/popular). Both compute their results into a feed cache; the API reads from that cache on request.
Start with source (1). When a channel uploads a video, a row lands in VideoMetadata
— but how does the system know
a row was added? That's Change Data Capture (CDC): monitoring a data source for changes and streaming them in real time to another service. The lesson weighs
four ways to do it:
| CDC approach | Verdict here |
|---|---|
Timestamp column
(LastUpdated
), polled periodically
| Doesn't fit — we add new rows, not update existing ones; and frequent polling loads the table. |
| Table snapshots, compared over time | Storage-intensive; won't scale with heavy writes. |
| Read the DB transaction log | Viable — changes are read from the log. |
| Trigger an event on change → push to Kafka [+] chosen | The approach used: a write triggers an event that moves the change to a Kafka queue asynchronously. |
Once the video streams into Kafka, take its ChannelId
and find the channel's subscribers. Instead of querying the ChannelSubscription
bridge table every time, cache it in a key-value store (Redis): key = ChannelId, value = list of subscriber UserIds. A module called the Channel Subscription
Feed
polls the Kafka queue and pushes the video into each subscriber's recommendation feed.
The recommendation object, in a key-value store#
Store each user's feed in a key-value store for O(1) access: key = UserId, value = a list of VideoRecommendation objects.
class VideoRecommendation { int id; List<Integer> videoIds; boolean polledByUser = false; // ...other fields }
When the Channel Subscription Feed pulls, say, 25 videos to recommend to a user, it adds them to
videoIds, sets polledByUser = false, wraps it in a VideoRecommendation object, and appends it to that user's list. When the user requests
recommendations, one or more objects are returned, polledByUser
flips to true, and once sent they can be discarded from the cache
to free space.
The second source: watch behavior
A Recommendation Engine
computes videos from viewing behavior (trending/popular included). It ingests VideoMetadata
via the same CDC-into-Kafka
flow for new videos. To surface trending content it must re-query the table for updated views/likes/comments —
so keep a copy of VideoMetadata
the engine queries continually, sparing the origin table load. The engine writes its results into the same
VideoRecommendation list. (The actual matching is machine learning, out of scope.)
[+] Swap "video" for "image" and this is exactly how a Pinterest or Instagram feed works too.
Part B · Push Notifications Service
Notifications: the same flow, reversed#
The notification workflow mirrors the feed's: a new VideoMetadata
row triggers an event → Kafka queue → a Notification Service
polls it, takes the ChannelId, looks up subscribers in the key-value store, and generates a notification object per subscriber:
class Notification { int id; String notificationMessage; Integer videoId; String videoURL; // ...other fields }
These land in each user's notification feed in a key-value store (key = UserId, value = list of Notification objects). The crucial difference from the feed: notifications are pushed, not pulled.
The backend pushes notification objects to the client over a persistent connection (an HTTP push technology). Compare YouTube: refreshing the home page pulls the recommendation feed, but new-video notifications are pushed. Notifications must arrive in real time as the event happens — you can't wait for the user to ask. What if they never send a request? Or request a live-event notification after the event ended? That defeats the purpose.
Two problems at ESPN/YouTube scale#
Don't bombard viewers with every event — send a curated few catered to their browsing/watch behavior, plus the important ones. This is exactly why YouTube introduced the bell icon: you only get notifications for channels where you pressed the bell. If a system has too many notifications to handle, introduce a similar opt-in, or prioritize by watch behavior with machine learning.
Part C · Content Management Service (CMS)
Picking a data model: knowledge graphs#
On a sports site, content is videos (already covered) plus articles with images. The CMS has two sub-services: a Content Storage Service (takes content from writers, persists it) and a Content Delivery Service (serves it to readers). An article has a title, summary, content, images, categories, tags, and one or more authors — with one-to-many relationships to categories (sport, tournament), tags (club, country, game type), and authors.
Which database? The lesson eliminates two candidates:
| Candidate | Why it struggles |
|---|---|
| Document DB (denormalized) | The main queries are "articles by author X / category Y / tag Z." To answer fast you'd need a separate collection per category, tag, and author — with massive data overlap (a baseball article duplicated across all its categories' and tags' collections). Undesirable. |
| Relational | A "better choice" than documents here — but deep, connected queries demand many joins, and latency climbs with relationship depth. |
| Graph (knowledge graph) [+] chosen | Store articles, categories, tags, authors as nodes with edges as relationships. Purpose-built for connected data. |
They power CMSs like Wikipedia and Bloomberg with big, complex data. A Wiki article on a historical war links continents, countries, battles, locations, commanders, allies, hostile forces — letting a reader wander from one battle to related events, people, and nations. That deep interlinking drives engagement and retention. Google and Facebook maintain knowledge graphs to surface relevant data from petabytes at low latency. Modeling all of history's wars in a document or relational DB would be a nightmare — denormalization exhausts disks and joins send latency "soaring like a NASA rocket."
Modeling the content graph#
A graph model has nodes, node properties, directed relationships, and labels. Authors (Ron, Jim, Nancy…), categories (Baseball, Football, Cricket, Boxing), tags (India, Ashes, Yankees, Cubs…), and articles all become nodes with directed edges between them. In Neo4j, nodes are created/queried with the Cypher query language (Cypher is to Neo4j what SQL is to a relational DB).
Two query types
-
Query 1
— "all articles by author Ron / in category Baseball / with tag Yankees." Locate the node and traverse its
edges (
AUTHORED,HAS AN ARTICLE, etc.). - Query 2 — "articles by Ron and in Baseball and tagged Yankees." Locate Ron, traverse to his articles, then scan those articles' relationships to the category and tag nodes.
Query 2 on a relational DB needs multiple joins — and the deeper the scan, the more joins. The graph needs none, so responses are dramatically quicker. That's the whole reason graphs win for connected data.
Tuning the graph
- Redundant properties — if Query 2 is very frequent, add categories/tags as properties on article nodes (as well as separate nodes), shrinking scan depth. Writes update multiple places, fine since tags rarely change.
-
Relationship properties
— edges carry metadata too: an
Author —WRITES ABOUT→ Baseballedge can note long-form / summaries / tweets. - Labels — group nodes by role (e.g. label all clubs playing football in Asia) to speed frequent queries.
- Node properties — authors carry id/name/designation/experience; articles carry id/length/content/image URLs. In Neo4j properties are stored key-value (node id → properties). If property data is large/complex, keep it in a document or relational DB and store a pointer in the node — a polyglot model.
Connected data links nodes that sit far apart. If Shreya likes author Ron, and Ron reads authors X and Y, the graph can recommend X and Y's articles to Shreya. Pulling that off relationally in near-real-time would need an abysmal number of joins — this is where graphs shine.
CMS core architecture#
Authors write in a UI and hit publish. The article goes to an Article Processor, which:
- Triggers a serverless event that, via a Kafka queue, sends the images to an Image Processing Service — the queue loosely couples the two so image processing works at its own pace.
- Persists the article to the graph database.
- Meanwhile the Image Processing Service compresses/enhances images and stores them in a cloud object bucket.
- On success, another serverless event updates the image URLs as properties on the article node in the graph DB.
- Once all events succeed, the Article Processor returns a publish-success response, and published content is cached per business logic.
You could also do it synchronously: optimize images, store them, update the article object with URLs, then persist everything to the graph DB. It doesn't matter — in both cases the Article Processor won't send success until the node's properties carry the image URLs.
Content delivery reuses the VOD pattern exactly: reader → edge cache (hit → served) → regional cache → content-serve service at the origin. No need to reinvent it.
Recap#
These three services are one idea seen from three angles: a write in one place has to become the right experience for millions of specific people. Change Data Capture turns a database write into a stream; Kafka fans that stream out without coupling anyone; and then the shape of the destination decides everything. A feed the user checks → cache it and let them pull. A moment they can't miss → push it, but curate so you don't drown them. Content whose value is its connections → store it as a graph so "related" is a traversal, not a pile of joins. Match the delivery to how the human actually consumes it.
Further reading#
Both external references resolve live. The author's own course cross-links (message queues, persistent connections, document/graph DBs, polyglot persistence) are omitted.