I've spent a good chunk of the last few years building community and social features — feeds, chat, moderation systems, notification pipelines — and I keep watching developers (including past me) make the same set of mistakes. Not because they're bad engineers, but because a community platform looks like a CRUD app and behaves like a distributed system with feelings.
This is the article I wish someone had handed me before my first build. No frameworks pitched, no product philosophy — just the engineering decisions that will quietly decide whether your platform survives contact with real users.
- The feed is not a query. Decide your fan-out strategy on day one.
Every community platform has a feed, and every naive implementation starts the same way:
sql
SELECT * FROM posts
WHERE author_id IN (SELECT followed_id FROM follows WHERE follower_id = ?)
ORDER BY created_at DESC
LIMIT 20;
This works beautifully in the demo. It works acceptably at 1,000 users. Somewhere around the point where your most-followed member has 50k followers and your follows table has tens of millions of rows, this query becomes the thing your on-call dreads.
The real decision is fan-out on write vs. fan-out on read:
Fan-out on write: when someone posts, push the post ID into a precomputed feed (usually Redis) for every follower. Reads are O(1) and instant. Writes explode for popular accounts — one post from a 100k-follower member means 100k list insertions.
Fan-out on read: compute the feed at request time (the query above, with heavy caching). Writes are cheap; reads get progressively more expensive.
The hybrid everyone lands on: fan-out on write for normal accounts, fan-out on read for "celebrity" accounts above a follower threshold, merge at read time.
You don't need the hybrid on launch day. You do need to structure your code so switching strategies isn't a rewrite — keep feed generation behind a single interface from the start. Retrofitting this into a system where six features query posts directly is a rewrite.
- Trust levels are the cheapest moderation system you will ever build
Here's the moderation feature with the best effort-to-impact ratio I've ever shipped, and it contains no ML whatsoever: graduated permissions based on account age and participation.
text
Level 0 (new): read, react. No links, no images, no DMs.
Level 1 (basic): post/comment, rate-limited. Links held for review.
Level 2 (member): normal posting, images, DMs.
Level 3 (regular): create topics/spaces, flag weight increased.
Level 4 (veteran): edit titles, move threads, moderate queues.
Spam bots and drive-by trolls share one trait: they won't invest three weeks of genuine participation to earn posting rights. Trust levels filter them out structurally, before your flag queue or your toxicity classifier ever sees them. Discourse has run on this model for a decade for good reason.
Implementation notes from scar tissue:
Compute levels asynchronously (a nightly job is fine), never inline on request.
Make thresholds config, not code. You will tune them.
Log every automated restriction with a reason. The first time a legitimate new user gets link-blocked, support needs to see exactly why.
Build the manual override on day one. Someone's CEO will sign up and need Level 2 immediately. This is not hypothetical.
- Your notification system is retention infrastructure. Build it like one.
The mistake: treating notifications as sendPush(userId, text) calls scattered across the codebase wherever events happen.
Six months later you have no per-user frequency caps, no quiet hours, no way to batch "17 people reacted" into one alert, and no way to let users mute anything without muting everything. Users respond rationally: they disable notifications at the OS level, and you've permanently lost your only re-engagement channel.
Structure it as a pipeline instead:
text
event bus → eligibility (prefs, mutes, caps)
→ aggregation window (collapse similar events)
→ scheduling (quiet hours, timezone)
→ delivery (push/email/in-app) → receipts
Every notifiable event goes onto the bus; a single service owns the rest. This costs maybe a week extra upfront and saves you a quarter of painful refactoring later.
One product-engineering detail worth stealing: prioritize the new user's first reply above almost everything. A member who gets a genuine response within 24 hours of their first post retains at wildly better rates than one who posts into silence. Some platforms literally route first posts into a special queue for volunteer greeters. That's not growth hacking; that's understanding what the system is for.
- Real-time is a spectrum. Buy the bottom of it, build the top.
"We need chat" is where budgets go to die. Persistent WebSocket connections, presence, typing indicators, message ordering, mobile reconnection over garbage networks, offline queueing — this is months of infrastructure work that produces something users consider table stakes.
My honest advice after building it both ways:
Buy or use OSS for the transport layer. Managed chat SDKs or self-hosted engines handle connection management, ordering, and sync — problems that are solved, undifferentiated, and brutal to reimplement well.
Build everything above it yourself: how chat ties into your permission system, trust levels, moderation queues, and notification pipeline. That integration layer is your product; the raw message plumbing is not.
Question presence indicators. "12 members online" is motivating in a busy community and devastating in a new one showing "1 member online" (it's the visitor, alone). Make presence display a config flag you can flip per-space.
- Design the empty room
Engineers test with seeded databases full of activity. Real communities launch empty, and the empty state is the state your earliest — most important — users actually experience.
Concretely, this means: feeds need a designed zero-state with evergreen content, not a blank scroll. Spaces need minimum-viable-density logic — better to launch with 2 channels that feel alive than 12 that feel abandoned (make channel creation an admin action, not a default). And your ranking algorithm needs a cold-start mode: chronological-with-pins works fine below a threshold of daily posts; engagement-ranked feeds need engagement to exist first.
I now consider "what does this screen show with 30 users and 4 posts?" a standard design review question, same as loading and error states.
- The boring list that saves you
Rapid-fire lessons that each cost me something to learn:
Soft-delete everything. Moderation disputes, GDPR requests, and "I deleted it by accident" all need deleted_at, not DELETE.
Store moderation actions as an append-only log. Who did what, to whom, why, reversible. Communities die from perceived unfairness faster than from actual trolls, and an audit trail is your only defense.
Rate-limit writes per trust level from day one. Adding rate limits after the first spam wave means doing it during the incident.
Media uploads need resumability. Your users are on phones, on mobile data, in elevators.
Search is a feature for the silent 90% who read but never post. Lurkers experience your community through search and digests. Build for them; they're most of your users.
The uncomfortable summary
None of the hard parts of a community platform are visible in a screenshot, and all of them are miserable to retrofit. Fan-out strategy, trust levels, the notification pipeline, moderation audit logs — these are day-one architecture decisions wearing the costume of "we'll add it later."
You don't have to build everything upfront. You have to decide everything upfront, and leave seams in the architecture where the deferred pieces will land. That's the entire trick. The communities that survive aren't running cleverer algorithms — they're running on foundations that someone poured before the users arrived.
If you've built in this space and hit different walls, I'd genuinely like to hear about them in the comments — especially anyone who's handled feed fan-out differently at scale.
Top comments (0)