At whereweup, we build mobile-first platforms on AWS serverless. For Leemo, our social discovery app, one of the hardest features to get right was the feed. Like Instagram or TikTok, Leemo’s feed mixes posts from followed profiles and recommendations, and it needs to be fast, real-time, and scalable.
In this post, we’ll break down the feed architecture we built in Golang, AWS Lambda, DynamoDB, and SQS, focusing on the fan-out pattern with batching and parallelism.
Our backend stack is fully serverless:
We store data in separate DynamoDB tables:
Media → user posts (PK = mediaId)Relation → follow graph (PK = userId, SK = followedUserId)Feed → fan-out results (PK = userId, SK = timestamp)MediaCounters, ProfileCounters → likes, comments, plays, etc.The naive approach—fetching posts from every followed user at feed load time—fails in DynamoDB because you can’t query across partitions efficiently. You’d need 1,000 queries if you follow 1,000 users, and results wouldn’t be globally ordered.
There are two patterns for building feeds:
Build the feed dynamically when the user opens the app. Cost scales with number of followed accounts. High latency, expensive, doesn’t scale.
On post creation, push a copy of that post into each follower’s Feed partition. Reading a feed = one query on Feed table:
Query Feed where PK = :userId ORDER BY timestamp DESC
We chose fan-out.
What happens when an influencer with 1 million followers posts?
The solution: batched fan-out with parallelism.
func FanOutToFollowers(ctx context.Context, sqsClient *sqs.Client, queueURL string, followers []string, mediaID, postedBy string, timestamp int64, batchSize int) error {
total := len(followers)
if total == 0 {
return nil
}
batchCount := (total + batchSize - 1) / batchSize
for i := 0; i < total; i += batchSize {
end := i + batchSize
if end > total {
end = total
}
msg := FanoutMessage{
FollowerIDs: followers[i:end],
MediaID: mediaID,
PostedBy: postedBy,
Timestamp: timestamp,
BatchNumber: (i / batchSize) + 1,
TotalBatches: batchCount,
}
body, _ := json.Marshal(msg)
_, err := sqsClient.SendMessage(ctx, &sqs.SendMessageInput{
QueueUrl: aws.String(queueURL),
MessageBody: aws.String(string(body)),
})
if err != nil {
return err
}
}
return nil
}
Feed(userId, timestamp).At whereweup, this architecture powers Leemo’s feed today, scaling from new users to influencers with millions of followers. If you’re building a social feed on AWS, fan-out with batching is a battle-tested approach.