MediumMongoDB or Cassandra? What Happens When a Celebrity With 200 Million Followers Joins Your App | by Dennis Peter Munyao | Nov, 2026 | Medium
Sitemap

MongoDB or Cassandra? What Happens When a Celebrity With 200 Million Followers Joins Your App

8 min readNov 1, 2026

--

I had a question stuck in my head recently that I couldn’t shake: if you built a social app on MongoDB, and a celebrity with 200 million followers signed up, what would actually break?

Not in theory. In practice. Which collection falls over first, which query gets slow, and what would you change?

It turns out the answer teaches you most of what you need to know about data modelling at scale. So here’s how I worked through it.

Press enter or click to view image in full size
One celebrity, 200 million followers, and a database decision that can make or break your app

The design everyone writes first

When you’re new to MongoDB, the document model feels like magic. No joins, no separate tables, everything about a thing lives in one place. So the first version of a user or a post usually looks something like this:

// The "obvious" first design: everything embedded in one document
public class User
{
public string Id { get; set; }
public string Name { get; set; }
public List<string> Followers { get; set; } = new(); // grows forever
}
public class Post
{
public string Id { get; set; }
public string Text { get; set; }
public List<string> Likes { get; set; } = new(); // grows forever
}

It reads beautifully. Want to know who follows someone? It’s right there. Want to know who liked a post? Same.

And for a small app, it works fine. The problem is that this design has a time bomb in it.

The 16 MB wall

MongoDB caps a single document at 16 MB. That sounds like a lot until you do the arithmetic.

A user ID stored as an ObjectId is 12 bytes, plus some overhead per array element. Call it roughly 20 to 30 bytes each. At that size, a single document runs out of room somewhere between half a million and a million entries.

Our celebrity has 200 million followers. That array doesn’t just exceed the limit; it’s hundreds of times over it.

But even before you hit the wall, things get ugly. Every time someone follows the celebrity, MongoDB has to update a document that keeps getting bigger. Every like on a viral post rewrites a growing array. The more popular something gets, the more expensive every single interaction with it becomes. That’s the opposite of what you want.

I call this the embedding trap. Embedding is one of MongoDB’s best features, but only for data that stays small.

The fix: one document per relationship

The solution is almost boring. Instead of storing followers inside the user, you store each follow as its own tiny document in a separate collection:

// Register once at startup so C# properties map to camelCase field names
ConventionRegistry.Register("camelCase",
new ConventionPack { new CamelCaseElementNameConvention() }, _ => true);
// follows collection: one small document per relationship
public class Follow
{
public ObjectId Id { get; set; }
public string FollowerId { get; set; } // e.g. "dennis"
public string FolloweeId { get; set; } // e.g. "celebrity"
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

Each document represents one fact: this person follows that person. That’s it.

People are sometimes surprised by this. “So if I follow 30,000 accounts, there are 30,000 documents about me?” Yes, but they don’t live on you. Your user document stays tiny. Those 30,000 documents sit in a shared collection alongside everyone else’s, and an index lets MongoDB jump straight to yours.

var follows = db.GetCollection<Follow>("follows");
await follows.Indexes.CreateManyAsync(new[]
{
// "who do I follow?" + blocks duplicate follows
new CreateIndexModel<Follow>(
Builders<Follow>.IndexKeys
.Ascending(f => f.FollowerId)
.Ascending(f => f.FolloweeId),
new CreateIndexOptions { Unique = true }),
// "who follows this person?"
new CreateIndexModel<Follow>(
Builders<Follow>.IndexKeys.Ascending(f => f.FolloweeId))
});

The first index answers “who do I follow?” and blocks duplicate follows. The second answers “who follows this person?”

Let’s sanity-check the size. 200 million follow documents at around 100 bytes each is roughly 20 GB, plus indexes. Spread across a sharded cluster, that’s not scary at all. Each document is small and never grows, so writes stay cheap no matter how famous someone gets.

For sharding, I’d use the follower as the shard key:

var admin = client.GetDatabase("admin");
await admin.RunCommandAsync<BsonDocument>(new BsonDocument
{
{ "shardCollection", "app.follows" },
{ "key", new BsonDocument("followerId", "hashed") }
});

The most frequent query is “who do I follow?” because that’s what builds your feed. With this shard key, it always hits a single shard. “Who follows the celebrity?” is rarer, mostly needed for notifications, so it can run as a background job.

Don’t count. Keep a counter.

Here’s a mistake that’s easy to make once you’ve split followers out: showing “200M followers” on a profile by running CountDocumentsAsync(). Counting 200 million documents every time someone opens a profile page is a great way to melt your database.

Instead, keep the number on the parent document and update it atomically:

await follows.InsertOneAsync(new Follow
{
FollowerId = followerId,
FolloweeId = followeeId
});
await users.UpdateOneAsync(
Builders<BsonDocument>.Filter.Eq("_id", followeeId),
Builders<BsonDocument>.Update.Inc("followerCount", 1));

Same idea for posts and likes. The post stores likeCount, and you increment it rather than recalculating it.

And if you want that little “Liked by Jane, Tom and 1.2M others” line, you can still embed, just carefully. MongoDB’s $slice (the slice argument of PushEach in the C# driver) lets you keep a fixed-size array of the most recent likers:

var update = Builders<BsonDocument>.Update
.Inc("likeCount", 1)
.PushEach(
"recentLikers",
new[] { new BsonDocument { { "userId", userId }, { "name", name } } },
slice: -3); // keep only the 3 most recent likers
await posts.UpdateOneAsync(
Builders<BsonDocument>.Filter.Eq("_id", postId),
update);

The array never holds more than three people. That’s the rule I keep coming back to: embed what’s small and bounded, reference what can grow forever.

The celebrity posts. Now what?

Storage is solved. The harder problem is what happens when the celebrity actually does something.

Get Dennis Peter Munyao’s stories in your inbox

Join Medium for free to get updates from this writer.

Most feed systems use something called fan-out on write. When you post, the system pushes your post ID into the feed of every one of your followers. For a normal person with a few hundred friends, that’s a few hundred quick writes, and then everyone’s feed is ready the moment they open the app.

Now do that for 200 million followers. Even at a million writes per second, one post takes over three minutes to distribute. And most of those followers aren’t even online. You’d be doing an enormous amount of work for people who won’t look at it for hours.

So you don’t. Big platforms use a hybrid approach:

  • Normal users fan out on write, exactly as before.
  • Celebrities don’t fan out at all. Their post is saved once, and its ID goes into a small cached list, something like celeb:{id}:posts in Redis.

When a follower opens the app, the feed service grabs their precomputed feed, checks which celebrities they follow, pulls those celebrities’ recent posts, merges everything, and ranks it. The celebrity’s post costs one write instead of 200 million.

You decide who counts as a celebrity with a simple flag. Cross some threshold, say 100,000 followers, and the account gets isCelebrity: true.

Hot keys and the thundering herd

There’s a catch. If 20 million people open the app in the hour after a celebrity posts, they’re all reading the same cached list. One Redis key, millions of reads. That’s a hot key, and a single node can’t take it.

A few tricks help:

  • Replicate the key. Store copies like celeb:{id}:posts:0 through :9 on different nodes and read a random one.
  • Cache in memory. Each feed server keeps a local copy of the celebrity lists, refreshed every few seconds. Most requests never touch Redis at all.
  • Coalesce requests. If a thousand requests miss the cache at the same instant, only one goes to the database, and the rest wait for its answer.

Interestingly, having more celebrities isn’t necessarily worse. Thirty celebrities with 10 million followers each spreads the load across thirty keys instead of one. Their recent post lists together are just a few kilobytes, small enough for every feed server to hold all of them in memory.

Surviving 100,000 likes per second

A viral post can attract likes faster than a single database node can comfortably write them. A few things keep it under control.

Buffer the writes. The like button doesn’t write straight to MongoDB. It drops an event onto a queue like Kafka and returns immediately. Workers drain the queue in batches at a pace the database can handle.

Shard the likes collection properly. If you shard by something like { postId: 1, userId: 1 }, or hashed, one viral post's likes spread across many shards instead of hammering one.

Shard the counter too. A single counter that gets incremented 100,000 times a second is itself a hot key. So you split it: likes:post123:0 up to :99, and each like increments a random one. To display the count, you sum them, cache the result for a second or two, and periodically write the total back to the post.

Will the count be a couple of seconds behind? Yes. Will anyone notice the difference between 1,204,331 and 1,204,512? No. This is eventual consistency doing exactly what it’s good at.

Comments follow the same thinking. Index them by { postId: 1, createdAt: -1 }, paginate with a cursor rather than Skip() (which gets painfully slow deep into a big list), and cache the first page of top comments. That first page is what nearly everyone reads anyway.

So is MongoDB the right tool for this?

I wondered about this too. A lot of system design write-ups reach for Cassandra when talking about likes and comments at Facebook scale, and there’s a good reason.

MongoDB uses a primary per shard. All writes for that shard go through one node. If the primary fails, there’s a brief pause while a new one is elected. Cassandra is leaderless: any replica can accept a write, so it keeps going even when nodes, or whole data centres, go down, and catches up later. For pure write volume across regions, that’s hard to beat.

But Cassandra makes you pay for it. You have to know your queries in advance, design a table for each one, and live with very limited querying. MongoDB is far more flexible and much nicer to build with.

My honest take: for a startup, or anything up to many millions of users, MongoDB handles followers, likes and comments comfortably, as long as you model them the way described here. The embedding trap is a modelling mistake, not a MongoDB limitation. If you ever reach the point where write volume genuinely outgrows it, you’ll have the traffic, the data, and hopefully the team to justify a move.

The lesson I took away

Most of the scary-sounding problems here came down to a handful of ideas:

  1. Embed what’s bounded, reference what isn’t. A post’s tags can live inside the post. Its likes cannot.
  2. Store counts, don’t compute them.
  3. Treat outliers differently. Celebrities, viral posts, and hot keys deserve their own path.
  4. Push work off the request path. Queues absorb spikes; users get a fast response.
  5. Accept eventual consistency where nobody will notice.

None of these are MongoDB-specific. You’d do the same things in SQL Server with a Follows table, or in Cassandra with its own tables. The database changes; the thinking doesn't.

And that’s really the point. System design looks complicated from a distance, but up close it’s the same small set of ideas, applied carefully, one bottleneck at a time.

--

--

Dennis Peter Munyao
Dennis Peter Munyao

Written by Dennis Peter Munyao

Engineer by profession, coder by passion. I solve complex problems, write clean code, and share insights on software development and tech trends.