Day 3: The Read Path – Timeline Reconstruction and Feed Logic
Welcome back, fellow architects and engineers! Today, we peel back another layer of the distributed systems onion, focusing on a critical yet often underestimated component: the Read Path. If our previous sessions laid the groundwork for how data gets into our system (the Write Path), today we explore the art and science of bringing that data back to life, specifically for a user's feed or timeline. This isn't just about fetching data; it's about reconstructing a coherent, timely narrative from potentially billions of data points, all while serving 100 million requests per second with razor-sharp latency.
The Read Path: A Different Beast
Think about your social media feed. It looks simple, right? A stream of posts, neatly ordered. But behind that simplicity lies a symphony of distributed services working in concert. Unlike the Write Path, which often prioritizes durability and eventual consistency, the Read Path for a user-facing feed demands low latency and high availability. Users expect their feed to load instantly, reflecting the latest updates from their network. This fundamental difference shapes our architectural choices.
Core Challenge: Timeline Reconstruction
Timeline Reconstruction is the process of assembling a personalized stream of content for a user. Imagine you follow 1,000 people. When you open your feed, the system needs to:
Identify everyone you follow.
Fetch the latest, say, 10-20 posts from each of those 1,000 people.
Aggregate all these posts.
Sort them by relevance or recency.
Return the top N posts.
At 100M RPS, if each request fans out to 1,000 other services, we're talking about trillions of internal requests per second. This "fan-out" problem is the Everest of read path scaling.
Feed Logic: The Art of Assembly
For our hands-on lesson, we'll start with a straightforward Feed Logic: reverse chronological order. This means the newest posts from the people you follow appear first. While real-world feeds use complex ranking algorithms (driven by machine learning, engagement, etc.), understanding the foundational chronological aggregation is paramount. It's the bedrock upon which all sophisticated feed algorithms are built.
Component Architecture: The Feed Service Ensemble
Our read path will revolve around a central Feed Service. This service doesn't store posts itself; it orchestrates the fetching and aggregation. It interacts with:
User Service: To get details about the requesting user.
Follower Service: To identify who the requesting user follows.
Post Service: To fetch actual post content from the followed users.
Cache: A critical component to absorb the fan-out storm.
How does this fit into the overall system? The Feed Service is the gateway to personalized content. It consumes data written by the Write Path (e.g., a user publishing a post via a "Post Write Service") and transforms it into a consumable stream for the end-user.
Data Flow & Control Flow:
User's device sends a
GET /feedrequest to the Load Balancer.Load Balancer routes it to an instance of the Feed Service.
Feed Service receives the request:
It queries the Follower Service to get a list of
followedUserIdsfor the requestinguserId.For each
followedUserId, it queries the Post Service to get their recent posts. This is where the fan-out happens.It aggregates all collected posts.
Sorts them chronologically (newest first).
Applies any basic filtering (e.g., remove duplicates, limit to N posts).
Returns the compiled feed to the user.
Unleashing Concurrency with Project Loom (Virtual Threads)
The fan-out problem is fundamentally an I/O-bound challenge. We're waiting for network calls to the Follower Service and many Post Service instances. This is where Java's Project Loom, with its Virtual Threads, shines like a beacon. Traditional platform threads are expensive, limiting the number of concurrent I/O operations. Virtual Threads, being lightweight, allow us to launch thousands or even millions of concurrent tasks without significant overhead, making the fan-out on read pattern far more manageable and efficient.
Instead of complex CompletableFuture chains or thread pools, Virtual Threads allow us to write sequential-looking code that executes concurrently.
Notice how Executors.newVirtualThreadPerTaskExecutor() allows us to treat each I/O bound task (fetching posts for a single user) as its own lightweight "thread," dramatically simplifying the concurrency logic compared to managing a fixed-size thread pool or callback hell. This is the high-concurrency simplicity Java 25/26 promises.
The Critical Role of Caching
While Virtual Threads make fan-out possible, they don't solve the problem of overwhelming downstream services. Imagine 100M RPS hitting your Feed Service, which then fans out to a Post Service. The Post Service would collapse. This is where caching becomes non-negotiable.
For timeline reconstruction:
Followed Users List: This list changes infrequently. It's a prime candidate for caching at the Feed Service level (e.g.,
followedUserCache.get(userId)).Recent Posts per User: The most critical cache. Instead of hitting the Post Service for every followed user every time, the Feed Service could query a local or distributed cache (like Redis or Memcached) that stores recently published posts for each user. This cache would be updated by the Write Path.
The insight here is that you're not just caching results; you're caching intermediate data to reduce the fan-out amplification. At 100M RPS, even a 99% cache hit rate for followed users and 90% for recent posts can reduce downstream load by orders of magnitude, making your system viable. The trade-off is data freshness. If a post is written, how long until it appears in the cache and then in the feed? This becomes a product decision.
Real-World Implications & Nuances
Latency Budgets: For 100M RPS, your end-to-end latency might be budgeted for 100-200ms. Each internal service call (like
followerService.getFollowedUsers) must complete in milliseconds. Virtual Threads help keep the orchestration fast, but the underlying service calls must be optimized.Partial Failures: What if one of the 1,000 followed users' Post Services is down? You can't let the entire feed request fail. Implement robust error handling (e.g.,
try-catcharoundfuture.get()), collect successful results, and log failures. Return a partially constructed feed; it's better than no feed.Thundering Herd: When a cache entry expires, many concurrent requests might try to re-populate it simultaneously. Implement cache-aside patterns with single-flight requests (only one request goes to the database/downstream service, others wait for its result).
This read path architecture, leveraging efficient concurrency and strategic caching, is the blueprint for handling massive scale. It moves beyond theoretical discussions to practical, implementable solutions.
Assignment: Enhance Your Feed Service
Your task is to extend the FeedService to incorporate a basic caching mechanism for followedUserIds and demonstrate its impact.
Steps:
Introduce a
Mapbased cache: InFeedService, add aMap<String, List<String>> followedUsersCacheto storeviewerId -> List<followedUserIds>.Implement Cache-Aside Logic:
Before calling
followerService.getFollowedUsers(viewerId), checkfollowedUsersCache.If found, return from cache.
If not found, call the
followerService, then store the result infollowedUsersCachebefore returning it.
Simulate Cache Refresh: Add a simple mechanism (e.g., a method
clearFollowedUsersCache()) to invalidate the cache. Observe how the first request after invalidation hits thefollowerService, and subsequent requests hit the cache.Measure Performance Impact: Add
System.out.printlnstatements to show when thefollowerServiceis actually called versus when the cache is hit, and observe the time savings.
Solution Hints:
Cache Implementation: A
ConcurrentHashMapis a good choice forfollowedUsersCacheif you want thread-safe access. For this simple exercise, a non-concurrentHashMapis fine if accessed within a single thread context or with external synchronization.Cache-Aside: Your
getFollowedUserslogic withinFeedServiceshould look like:
Testing: Run
getFeedmultiple times for the sameviewerIdto see cache hits. Then callclearFollowedUsersCache()and run again to simulate a cache miss.
By completing this assignment, you'll gain a tangible understanding of how caching dramatically reduces load and improves performance in a fan-out scenario, a crucial insight for any high-scale distributed system.