User/Tweet/Follow/Timeline classes at the class-design level — distinct from the HLD fan-out architecture — and where pagination and caching hooks belong in this class model.
Published September 23, 2026
The Design Twitter / X system-design case covers HLD architecture — fan-out strategy, Redis timeline caching, feed ranking at scale. This lesson is the class-level design underneath that architecture — worth doing deliberately separately, since an interviewer can ask either one independently.
class User { String id; String username; }
class Tweet {
String id;
User author;
String content;
Instant createdAt;
List<String> likedByUserIds;
}
class FollowRelationship { User follower; User followee; Instant followedAt; }
interface FollowGraph {
void follow(User follower, User followee);
List<User> getFollowees(User user); // who does this user follow
List<User> getFollowers(User user); // who follows this user
}
class Timeline {
User owner;
List<Tweet> tweets; // this user's feed, in some defined order
}
interface TimelineService {
Timeline getTimeline(User user, int page, int pageSize);
}
class NaiveTimelineService implements TimelineService {
public Timeline getTimeline(User user, int page, int pageSize) {
List<User> followees = followGraph.getFollowees(user);
List<Tweet> allTweets = followees.stream()
.flatMap(f -> tweetRepository.findByAuthor(f).stream())
.sorted(Comparator.comparing(Tweet::getCreatedAt).reversed())
.toList();
return new Timeline(user, paginate(allTweets, page, pageSize));
}
}
This naive version pulls every followee's tweets and merges/sorts them at read time — correct, but exactly the fan-out-on-read approach the HLD case names as expensive at scale (fanning in across potentially hundreds of followees on every single timeline load). Stating this explicitly — "this class-level design is correct but naive; the HLD case's fan-out-on-write precomputation is what makes this fast at scale" — is precisely the LLD-to-HLD connection point the backlog prompt calls out directly: the fan-out strategy is genuinely an HLD-level concern (where does precomputed timeline data actually live, how is it kept warm), but the class shape (TimelineService as an interface, Timeline as a data holder) accommodates either a naive or a fan-out-optimized implementation without changing its own contract.
class CachedTimelineService implements TimelineService {
private final TimelineService delegate;
private final TimelineCache cache; // e.g. backed by Redis in the real HLD design
public Timeline getTimeline(User user, int page, int pageSize) {
Timeline cached = cache.get(user, page);
if (cached != null) return cached;
Timeline fresh = delegate.getTimeline(user, page, pageSize);
cache.put(user, page, fresh);
return fresh;
}
}
Both pagination (the page/pageSize parameters, ideally evolved to cursor-based per Design a News Feed System's pagination discussion) and caching (wrapping TimelineService via Decorator, the same shape as Movie Ticket Booking's layering) are natural extension points on the TimelineService interface itself, not changes to User/Tweet/FollowGraph. This is exactly why defining TimelineService as an interface early matters — it's the seam where the HLD-level caching/fan-out architecture actually plugs into this LLD class model, without either side needing to know the other's internal details.
Q: Why is FollowGraph its own interface/abstraction rather than a plain field on User? A: Follow relationships at Twitter's actual scale (millions of follows per popular account) don't fit reasonably as an in-memory list on a User object — abstracting it behind an interface means the underlying implementation (a graph database, a relational join table, a specialized service) can change without User's own class definition needing to change.
Q: How would 'liking' a tweet interact with concurrent likes from many users simultaneously? A: The same check-then-act concurrency concern as elsewhere in this course — a naive likedByUserIds.add() on a shared list needs the same kind of protection (a thread-safe collection, or moving the like-count to a dedicated atomic counter/service) that HashMap Concurrency Variants and Concurrent Utilities & Coordination cover generally.
Q: Should Tweet be immutable once created? A: Largely yes for content (tweets aren't editable on the real platform) — but likedByUserIds and similar engagement data genuinely need to mutate, arguing for Tweet holding immutable core content plus a reference to a separately-mutable engagement-tracking structure, rather than either a fully mutable or fully immutable Tweet class.
Q: Does this design's Timeline class assume a single global ranking, or could it support personalized ranking later? A: TimelineService returning a Timeline gives room for this — a RankedTimelineService implementation could apply a ranking algorithm on top of NaiveTimelineService's chronological output before returning, matching the 'feed ranking as a separate concern' principle from Design a News Feed System, expressed here as yet another swappable TimelineService implementation.