Building Activity Feeds and Social Features Into Your Product

Veld Systems||7 min read

Activity feeds look simple on the surface. A chronological list of events: "John commented on your post," "Sarah shared a document," "Your order shipped." But behind that simple list is one of the most architecturally challenging features in application development. Feeds touch real time delivery, fan out strategies, read optimization, notification preferences, and performance at scale. Getting it wrong means slow load times, missing events, notification spam, and frustrated users.

We have built activity feed systems for products ranging from project management tools to social platforms, and the pattern that works at 100 users rarely works at 100,000. This post covers the architecture decisions that determine whether your feed system scales gracefully or becomes a performance bottleneck.

The Core Data Model

Every feed system starts with events (also called activities). An event has a structure: actor performed action on object, optionally with a target. "Sarah (actor) commented (action) on your document (object) in the Design workspace (target)."

The naive approach is a single events table with columns for actor, action, object type, object ID, and timestamp. This works for small products but breaks down in two specific ways.

First, querying a user feed becomes expensive. If User A follows 500 other users, fetching their feed means querying all events where the actor is one of those 500 users, sorting by timestamp, and paginating. At scale, this query gets slow even with proper indexing.

Second, different users see different feeds. A project management tool might show Alice events from the three projects she belongs to, while Bob sees events from his seven projects. The query to assemble each feed is different, and running these complex filtered queries on every page load is not sustainable.

This is why most serious feed systems adopt one of two strategies: fan out on write or fan out on read.

Fan Out on Write vs Fan Out on Read

Fan out on write means when an event happens, you immediately write a copy of that event into every follower feed. If Sarah has 1,000 followers and she posts a comment, you write 1,000 rows, one for each follower feed. Reading a feed becomes trivial: just query the feed table for the current user, ordered by timestamp. Reads are fast because the data is pre computed.

The tradeoff is write amplification. High follower counts mean lots of writes per event. A user with 100,000 followers generates 100,000 writes per activity. This is manageable with background processing, but it requires a robust system architecture and job queue.

Fan out on read means you store events once and compute the feed at read time. When Alice opens her feed, you query for events from all users and projects she follows, merge and sort them, and return the result. Writes are simple, one row per event, but reads are complex and expensive.

In practice, most production systems use a hybrid approach. Fan out on write for users with normal follower counts (under 10,000), and fan out on read for users with massive followings (celebrities, official accounts). Twitter popularized this pattern, and it remains the most practical architecture for social products at scale.

For most B2B and SaaS products, pure fan out on write is the right choice. Your users typically follow tens to hundreds of entities, not millions. The write amplification is modest, and the read performance is excellent.

Real Time Delivery

A feed that only updates on page refresh feels stale. Users expect to see new activity appear in real time. There are three delivery mechanisms: polling, Server Sent Events (SSE), and WebSockets.

Polling is the simplest. Every 10 to 30 seconds, the client asks the server "any new events since my last check?" This works but creates unnecessary load and has inherent latency. If you poll every 15 seconds, events are on average 7.5 seconds old when the user sees them.

SSE is a one way channel from server to client. The server pushes new events as they happen. It is simpler than WebSockets because it uses standard HTTP and automatically handles reconnection. For activity feeds where the data flow is primarily server to client, SSE is often the best choice.

WebSockets provide a full duplex channel. They are necessary if users are also sending data back to the server in real time, like in a chat application. For pure activity feeds, WebSockets are more complexity than you need. We covered the tradeoffs between these approaches in our real time architecture guide.

Whichever mechanism you choose, the architecture pattern is the same. When a new event is created, it gets published to a message bus (Redis Pub/Sub, a message queue, or a similar system). Connected clients subscribed to that channel receive the event and prepend it to their feed UI.

Aggregation and Grouping

Raw event feeds quickly become noisy. If 15 people like your photo, you do not want 15 separate "X liked your photo" entries. You want one entry that says "Sarah, John, and 13 others liked your photo."

Aggregation is the process of grouping related events into a single feed entry. The grouping logic depends on your product. Common aggregation rules:

Same action on the same object within a time window. Multiple likes on the same post within an hour become one aggregated entry. Multiple comments do not aggregate because each comment has unique content worth showing.

Same actor performing multiple actions. "Sarah uploaded 12 photos to the Design album" instead of 12 separate upload events.

Threshold based display. Show individual actors up to 3, then switch to "and N others." This keeps entries scannable.

Aggregation can happen at write time (group events as they arrive) or at read time (group events when rendering the feed). Write time aggregation is more efficient but harder to update when new events join an existing group. Read time aggregation is simpler to implement but adds processing overhead to every feed load.

Notification Preferences

The fastest way to make users hate your feed system is to spam them. Every notification channel, in app, email, push, needs granular user controls. At minimum, users should be able to:

Choose notification channels per event type. "Send me an email for comments on my documents, but only show likes in the app feed."

Set frequency. Immediate, hourly digest, daily digest, or off. Digests require a background job that batches events and sends a summary on schedule.

Mute specific sources. "Stop showing me events from this project" without unfollowing entirely.

The preferences data model is a matrix of event types and channels. Store it as a JSON column or a dedicated preferences table with rows for each event type and channel combination. Default to sensible settings that are not aggressive, users who feel spammed on day one unsubscribe and do not come back.

The Notification Infrastructure Layer

Beyond the feed UI, you likely need to deliver notifications through multiple channels. Push notifications for mobile, email for important events, and in app badges for unread counts. Each channel has its own delivery mechanism and failure modes.

In app notifications are the simplest. Store them in a notifications table with a read/unread flag. Display an unread count badge in the UI and mark as read when the user views them.

Email notifications should never be sent synchronously. Queue them and send via a background job. Include an unsubscribe link in every email (it is legally required in most jurisdictions and also good practice). Batch low priority events into digests to avoid inbox flooding.

Push notifications require integration with Apple Push Notification Service (APNs) and Firebase Cloud Messaging (FCM). Both are unreliable enough that you need to handle failures and retries. Neither guarantees delivery. Design your system so push notifications are supplementary, not the only way users learn about important events.

Performance at Scale

Feed performance comes down to two things: how fast you can write events and how fast you can read feeds.

For writes, the answer is always asynchronous processing. The action that generates the event (posting a comment, uploading a file) should return immediately. The event creation, fan out, and notification delivery all happen in background jobs. This keeps your application responsive even when a single action triggers thousands of downstream writes.

For reads, the answer is caching and pagination. Cache the most recent N events in each user feed using Redis or a similar in memory store. Most users only look at the first page of their feed, so caching the top 100 entries covers 95% of requests. Cursor based pagination (using event timestamps or IDs as cursors) is more efficient than offset based pagination for feeds that update frequently.

If your product is in the early stages and you are weighing custom development versus SaaS for social features, consider this: third party feed services like Stream and Knock exist, and they handle the infrastructure complexity. But they come with per event pricing that gets expensive at scale, limited customization, and vendor lock in. For products where the feed is a core feature, building custom is usually the right long term choice.

Start Simple, Design for Growth

Do not over engineer your first version. Start with a simple events table, fan out on read, and polling for updates. This architecture handles thousands of users without issue. Introduce fan out on write, real time delivery, and aggregation as your usage grows and you have data on actual usage patterns.

The important thing is to design the data model correctly from the start. Changing your event schema later is painful. Changing your delivery mechanism or fan out strategy is a backend refactor that does not affect your data model.

If you are building a product with activity feeds, social features, or notification systems, reach out and let us help you design an architecture that works today and scales tomorrow.

Ready to Build?

Let us talk about your project

We take on 3-4 projects at a time. Get an honest assessment within 24 hours.