Designing a Production-Grade Notification System (Like LinkedIn)
How I designed and implemented a scalable, real-time notification system using PostgreSQL, Redis, and Socket.IO — inspired by LinkedIn's engagement architecture.
introduction
Notifications appear simple from a product perspective, but implementing them at scale quickly becomes a distributed systems challenge. In LinkedUp, I designed a real-time notification system capable of handling user engagement events such as likes and comments while maintaining low latency and efficient unread tracking. The architecture combines PostgreSQL for durable storage, Redis for fast counters, and WebSockets for real-time delivery.
problem statement
A naive notification implementation can quickly become inefficient. Continuously querying the database for unread counts, sending unnecessary self-notifications, or relying solely on polling for updates introduces scalability and performance problems. For a social platform, notifications must be real-time, persistent, and efficient under high engagement.
naive approach and limitations
- Polling the database repeatedly for new notifications.
- Running COUNT queries for unread notifications on every request.
- Sending notifications even when users trigger actions on their own content.
- Lack of separation between event creation and delivery.
design decision
{
"persistent_storage": "PostgreSQL notifications table",
"real_time_delivery": "Socket.IO WebSocket events",
"unread_counter": "Redis key-based counter",
"event_generation": "Triggered on meaningful engagement events"
}architecture overview
The system follows a hybrid architecture. PostgreSQL stores all notifications as the durable source of truth. Redis maintains lightweight unread counters for instant retrieval. Socket.IO handles real-time notification delivery by emitting events to user-specific rooms. This combination ensures durability, performance, and real-time responsiveness.
implementation details
{
"database_design": "A notifications table stores each notification event with metadata such as actor_id, recipient_id, entity_id, and type. Indexing recipient_id with created_at allows efficient retrieval of recent notifications.",
"event_generation": "Notifications are created only for meaningful engagement events such as likes and comments. The system explicitly prevents self-notifications by checking if actor_id equals recipient_id before inserting a record.",
"redis_unread_counter": "Unread counts are stored in Redis using a key format like notif:count:{userId}. This avoids repeated database COUNT queries and allows instant unread count retrieval.",
"websocket_delivery": "Each connected user joins a private WebSocket room identified by their user ID. When a notification is created, the server emits an event to the corresponding room, enabling immediate UI updates."
}example query pattern
Unread counts are retrieved directly from Redis while notification lists are fetched from PostgreSQL using indexed queries ordered by creation time.
performance considerations
- Redis counters reduce database load from frequent unread count queries.
- Indexes on recipient_id and created_at enable efficient pagination.
- WebSocket rooms isolate notification delivery to specific users.
- Stateless services allow horizontal scaling.
security and validation
- Prevented self-notifications to avoid unnecessary events.
- Validated event sources before generating notifications.
- Restricted WebSocket channels to authenticated users.
tradeoffs
{
"pros": [
"Real-time notification delivery.",
"Efficient unread count retrieval.",
"Durable storage for notification history.",
"Horizontally scalable architecture."
],
"cons": [
"Additional infrastructure complexity due to Redis and WebSockets.",
"Requires careful synchronization between Redis counters and database state.",
"Real-time systems require handling offline users."
]
}why not polling
- Polling introduces unnecessary network requests.
- Higher database load due to repeated queries.
- Slower user experience compared to push-based updates.
lessons learned
- Separating persistence, caching, and delivery improves system scalability.
- Real-time systems must still support offline recovery.
- Redis works best for ephemeral performance optimizations rather than persistent data.
- Designing with horizontal scalability in mind prevents future bottlenecks.
future improvements
- Notification aggregation such as "3 people liked your post".
- Background workers for batching events.
- User-level notification preferences and mute settings.
- Event streaming architecture using Kafka for large-scale decoupling.
conclusion
By combining PostgreSQL for durable storage, Redis for fast unread counters, and WebSockets for instant delivery, the notification system achieves both reliability and real-time performance. This architecture mirrors production-grade social platforms and demonstrates how combining multiple infrastructure components can solve complex engagement problems efficiently.