How to Design Professional Network like LinkedIn
Building social graph, job matching, feed, and messaging at 1B+ member scale — A Senior+ Guide by Ayodhyya
Table of Contents
- Introduction — LinkedIn at Scale
- Functional & Non-Functional Requirements
- Capacity Estimation & Back-of-Envelope
- Data Model Design
- API Design
- High-Level Architecture
- Connection Graph & Degree Separation
- News Feed Generation
- Job Matching & Recommendation
- Profile System & Endorsements
- Messaging & InMail
- Company Pages & Employer Branding
- Content Publishing Platform
- Search System
- Notification System
- Recruiter Tools & Pipeline
- Learning Platform (LinkedIn Learning)
- Ads & Sponsored Content
- Database Sharding Strategy
- Caching Strategy
- Multi-Region Design
- Cost Estimation
- Interview Q&A (10+ Questions)
- Full C# Implementation (300+ Lines)
- Conclusion
1. Introduction — LinkedIn at Scale
LinkedIn is the world's largest professional networking platform, connecting over 1 billion members across 200+ countries and territories. With more than 65 million companies maintaining pages, 20+ million job listings active at any given time, and over 100,000 articles published daily, LinkedIn represents one of the most complex social systems ever engineered. It is not merely a social network — it is the global professional identity layer of the internet.
The platform handles a staggering range of features: a connection-based social graph, a personalized news feed, real-time messaging, a massive job marketplace with AI-powered matching, a content publishing platform (LinkedIn Articles and newsletters), a learning management system (LinkedIn Learning with 21,000+ courses), recruiter tools, company pages, advertising infrastructure, and an endorsements/recommendations engine. Each of these subsystems presents unique distributed systems challenges.
LinkedIn combines nearly every distributed systems concept: graph databases, event-driven architectures, recommendation engines, real-time messaging, search at scale, ML-powered ranking, and multi-region deployment. Designing a LinkedIn-like system is a favorite interview question at FAANG+ companies because it tests breadth across dozens of architectural patterns simultaneously.
In this comprehensive guide, we will decompose LinkedIn's architecture into its constituent subsystems, analyze the data models, estimate capacity requirements, design APIs, construct Mermaid architecture diagrams, and build production-grade C# implementations for the core services. By the end, you will have a senior-level understanding of how to architect a professional network capable of serving a billion users with sub-second response times.
The key numbers that define LinkedIn's scale include: 61 million daily active users searching for jobs, 8 billion profile views per week, 100 million job applications submitted monthly, 40 billion feed impressions per day, and over 10 billion messages exchanged monthly. These numbers drive every architectural decision — from the choice of storage engines to the number of cache clusters required.
2. Functional & Non-Functional Requirements
Functional Requirements
| Feature | Description | Priority |
|---|---|---|
| User Registration & Profiles | Create accounts, manage professional profiles with work history, education, skills, and media | P0 |
| Connection Management | Send, accept, reject connection requests; 1st, 2nd, 3rd degree network | P0 |
| News Feed | Personalized feed with posts from connections, companies, and content creators | P0 |
| Messaging | Direct messaging between connections, InMail for premium, group conversations | P0 |
| Job Listings & Applications | Post jobs, search/apply, job alerts, recruiter matching | P0 |
| Search | People, jobs, companies, content search with filters and autocomplete | P0 |
| Notifications | Push, email, in-app notifications for connections, jobs, messages, mentions | P1 |
| Company Pages | Company profiles, employee count, job postings, follower management | P1 |
| Endorsements & Recommendations | Skill endorsements, written recommendations between professionals | P1 |
| Content Publishing | Articles, short-form posts, document carousels, polls, newsletters | P1 |
| Recruiter Tools | Talent pipeline, candidate search, outreach tracking, hiring analytics | P1 |
| LinkedIn Learning | Video courses, assessments, certificates, learning paths | P2 |
| Advertising Platform | Sponsored content, job ads, message ads, dynamic ads, campaign analytics | P2 |
| Who Viewed Your Profile | Profile view tracking and analytics (premium feature) | P2 |
Non-Functional Requirements
| Requirement | Target | Rationale |
|---|---|---|
| Availability | 99.99% (52 min downtime/year) | Professional network used during business hours globally |
| Latency (p99) | < 200ms for reads, < 500ms for writes | Real-time feel for feed, messaging, and search |
| Throughput | 10M+ requests/second peak | Billions of profile views, feed loads, and searches daily |
| Data Durability | 99.999999999% (11 nines) | Professional data, messages, and recommendations are irreplaceable |
| Consistency | Eventual consistency for feed; strong for messaging and job applications | Feed can tolerate staleness; financial/transactional features cannot |
| Scalability | Linear horizontal scaling for all services | Growth from 1B to 2B+ members must not require re-architecture |
| Security | SOC 2, GDPR compliant, end-to-end encryption for messages | Professional data is highly sensitive |
3. Capacity Estimation & Back-of-Envelope
Read/Write Estimates
Assuming 1 billion registered members with 350 million monthly active users and 100 million daily active users, let us estimate the traffic volumes for each core subsystem.
| Operation | Daily Volume | QPS (avg) | QPS (peak, 3x) |
|---|---|---|---|
| Profile Views | 8 billion | ~93,000 | ~280,000 |
| Feed Loads | 10 billion | ~116,000 | ~350,000 |
| Connection Requests | 50 million | ~580 | ~1,740 |
| Messages Sent | 10 billion | ~116,000 | ~350,000 |
| Job Applications | 100 million | ~1,160 | ~3,480 |
| Search Queries | 2 billion | ~23,000 | ~70,000 |
| Post Creations | 500 million | ~5,800 | ~17,400 |
| Notifications Sent | 30 billion | ~347,000 | ~1,040,000 |
Storage Estimates
Profiles: 1B members × 5KB avg profile = ~5 TB
Connections: Average 500 connections/member × 1B = 500B edges × 50 bytes = ~25 TB
Posts/Content: 500M posts/day × 2KB avg = ~1 TB/day = ~365 TB/year
Messages: 10B messages/day × 1KB avg = ~10 TB/day = ~3.65 PB/year
Job Listings: 20M active × 10KB = ~200 GB (relatively small)
Profile Photos: 1B × 200KB = ~200 TB
Total Annual Storage Growth: ~4+ PB/year (messages and content dominate)
Conclusion: Blob storage for media and column-family stores for messages dominate the storage story.
Bandwidth Estimates
Incoming bandwidth (writes): ~500M posts/day × 2KB + 10B messages/day × 1KB + media uploads = approximately 15 TB/day incoming or about 175 MB/s average. Outgoing bandwidth (reads) is much larger due to caching: estimated at 50 TB/day or approximately 580 MB/s average, with peaks reaching several GB/s during US business hours.
4. Data Model Design
LinkedIn's data model spans multiple storage paradigms. We need relational stores for structured data (profiles, jobs), graph stores for the connection network, wide-column stores for messages, search indexes for discovery, and blob stores for media content.
Entity-Relationship Overview
Core Tables
| Table | Primary Key | Partition Key | Key Fields | Estimated Size |
|---|---|---|---|---|
| users | user_id (UUID) | user_id | email, name, password_hash, status, created_at | ~500 GB |
| profiles | user_id | user_id | headline, summary, location, industry, skills[], experience[], education[] | ~5 TB |
| connections | connection_id | sender_id | sender_id, receiver_id, status, created_at | ~25 TB |
| posts | post_id | author_id | author_id, content, media_urls, visibility, created_at, engagement_counts | ~2 TB |
| messages | message_id | conversation_id | conversation_id, sender_id, body, attachments, read_status, sent_at | ~100+ TB |
| conversations | conversation_id | conversation_id | participants[], last_message_at, is_group, title | ~500 GB |
| job_listings | job_id | company_id | company_id, title, description, location, salary_range, requirements, status | ~200 GB |
| job_applications | application_id | job_id | job_id, applicant_id, resume_url, status, applied_at | ~1 TB |
| companies | company_id | company_id | name, industry, size, logo_url, description, website | ~50 GB |
| notifications | notification_id | user_id | user_id, type, payload, read_status, created_at | ~5 TB |
| endorsements | endorsement_id | endorsed_user_id | endorsed_user_id, endorser_id, skill_name, created_at | ~500 GB |
C# Data Transfer Objects
public class UserProfile
{
public Guid UserId { get; set; }
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string Headline { get; set; } = string.Empty;
public string Summary { get; set; } = string.Empty;
public string Location { get; set; } = string.Empty;
public string Industry { get; set; } = string.Empty;
public string ProfilePhotoUrl { get; set; } = string.Empty;
public string BannerImageUrl { get; set; } = string.Empty;
public int ConnectionCount { get; set; }
public List<WorkExperience> Experience { get; set; } = new();
public List<Education> Education { get; set; } = new();
public List<Skill> Skills { get; set; } = new();
public ProfileVisibility Visibility { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
public class WorkExperience
{
public Guid Id { get; set; }
public string CompanyName { get; set; } = string.Empty;
public Guid? CompanyId { get; set; }
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public DateTime StartDate { get; set; }
public DateTime? EndDate { get; set; }
public bool IsCurrent { get; set; }
}
public class Education
{
public Guid Id { get; set; }
public string SchoolName { get; set; } = string.Empty;
public string Degree { get; set; } = string.Empty;
public string FieldOfStudy { get; set; } = string.Empty;
public DateTime StartDate { get; set; }
public DateTime? EndDate { get; set; }
}
public class Skill
{
public string Name { get; set; } = string.Empty;
public int EndorsementCount { get; set; }
}
public enum ProfileVisibility
{
Public,
ConnectionsOnly,
Private
}
5. API Design
LinkedIn's API follows RESTful conventions with versioned endpoints, OAuth 2.0 authentication, and rate limiting. Internal services communicate via gRPC for low latency, while the public API uses HTTPS/JSON. Let us define the key endpoints.
Profile APIs
| Method | Endpoint | Description | Rate Limit |
|---|---|---|---|
| GET | /api/v2/users/{userId}/profile | Fetch user profile | 1000/min |
| PUT | /api/v2/users/{userId}/profile | Update profile fields | 100/min |
| POST | /api/v2/users/{userId}/profile/photo | Upload profile photo | 10/hour |
| GET | /api/v2/users/{userId}/experience | List work experience | 500/min |
| POST | /api/v2/users/{userId}/experience | Add work experience | 50/min |
Connection APIs
| Method | Endpoint | Description | Rate Limit |
|---|---|---|---|
| GET | /api/v2/users/{userId}/connections | List connections (paginated) | 200/min |
| POST | /api/v2/connections/request | Send connection request | 50/day (free), 200/day (premium) |
| PUT | /api/v2/connections/{connId}/accept | Accept connection request | 200/min |
| DELETE | /api/v2/connections/{connId} | Remove connection | 50/min |
| GET | /api/v2/users/{userId}/network/degree/{targetId} | Get degree of separation | 100/min |
Feed & Post APIs
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v2/feed?cursor={cursor}&limit=20 | Get personalized feed |
| POST | /api/v2/posts | Create a new post |
| GET | /api/v2/posts/{postId} | Get single post with engagement |
| POST | /api/v2/posts/{postId}/reactions | React to a post (like, celebrate, etc.) |
| POST | /api/v2/posts/{postId}/comments | Comment on a post |
| GET | /api/v2/users/{userId}/feed?cursor={cursor} | Get user's post history |
Job APIs
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v2/jobs/search?q={query}&location={loc} | Search jobs with filters |
| POST | /api/v2/jobs | Post a new job listing |
| GET | /api/v2/jobs/{jobId} | Get job details |
| POST | /api/v2/jobs/{jobId}/apply | Apply to a job |
| GET | /api/v2/users/{userId}/job-applications | List user's applications |
| GET | /api/v2/jobs/recommendations | Get ML-powered job recommendations |
Messaging API
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v2/conversations?cursor={cursor} | List conversations |
| POST | /api/v2/conversations | Create new conversation |
| GET | /api/v2/conversations/{convId}/messages?cursor={cursor} | Get messages in conversation |
| POST | /api/v2/conversations/{convId}/messages | Send a message |
| PUT | /api/v2/conversations/{convId}/read | Mark conversation as read |
| POST | /api/v2/inmail | Send InMail (premium) |
6. High-Level Architecture
The LinkedIn platform is composed of hundreds of microservices organized into domain-aligned teams. The architecture follows an event-driven model with Apache Kafka as the central nervous system, connecting dozens of services through a publish-subscribe messaging backbone.
7. Connection Graph & Degree Separation
The connection graph is the foundation of LinkedIn's value proposition. With 1 billion nodes and an average of 500+ edges per node, the graph contains hundreds of billions of edges. Storing and traversing this graph efficiently is one of LinkedIn's greatest engineering challenges.
Graph Storage Model
LinkedIn uses a custom graph storage system called Terminus (historically known as Social Graph Service or SGS). The graph is stored as an adjacency list, partitioned by node ID across thousands of machines. Each partition stores a subset of nodes along with their complete adjacency lists.
Degree of Separation Algorithm
Finding the degree of separation between two users is a BFS (Breadth-First Search) problem on the social graph. LinkedIn limits this to 3 degrees (LinkedIn shows 1st, 2nd, and 3rd connections). For performance, the search is bounded and optimized with bi-directional BFS from both source and target.
public class DegreeOfSeparationCalculator
{
private readonly IGraphService _graphService;
private readonly IDistributedCache _cache;
private const int MaxDegree = 3;
private const int MaxNodesPerLevel = 100_000;
public DegreeOfSeparationCalculator(IGraphService graphService, IDistributedCache cache)
{
_graphService = graphService;
_cache = cache;
}
public async Task<DegreeResult> CalculateAsync(Guid sourceUserId, Guid targetUserId)
{
if (sourceUserId == targetUserId)
return new DegreeResult { Degree = 0, Path = new List<Guid> { sourceUserId } };
var cacheKey = $"degree:{Math.Min(sourceUserId, targetUserId)}:{Math.Max(sourceUserId, targetUserId)}";
var cached = await _cache.GetStringAsync(cacheKey);
if (cached != null)
return JsonSerializer.Deserialize<DegreeResult>(cached)!;
var result = await BidirectionalBfsAsync(sourceUserId, targetUserId);
if (result.Degree > 0)
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(result),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) });
return result;
}
private async Task<DegreeResult> BidirectionalBfsAsync(Guid source, Guid target)
{
var sourceQueue = new Queue<Guid>();
var targetQueue = new Queue<Guid>();
var sourceVisited = new Dictionary<Guid, (Guid? parent, int level)>();
var targetVisited = new Dictionary<Guid, (Guid? parent, int level)>();
sourceQueue.Enqueue(source);
sourceVisited[source] = (null, 0);
targetQueue.Enqueue(target);
targetVisited[target] = (null, 0);
int sourceLevel = 0;
int targetLevel = 0;
while (sourceQueue.Count > 0 || targetQueue.Count > 0)
{
if (sourceLevel <= targetLevel && sourceQueue.Count > 0)
{
var result = await ExpandLevelAsync(sourceQueue, sourceVisited, sourceLevel + 1);
sourceLevel++;
foreach (var nodeId in result)
{
if (targetVisited.ContainsKey(nodeId))
{
var degree = sourceLevel + targetVisited[nodeId].level;
if (degree <= MaxDegree)
return BuildResult(source, target, sourceVisited, targetVisited, nodeId);
}
}
}
if (targetQueue.Count > 0)
{
var result = await ExpandLevelAsync(targetQueue, targetVisited, targetLevel + 1);
targetLevel++;
foreach (var nodeId in result)
{
if (sourceVisited.ContainsKey(nodeId))
{
var degree = sourceLevel + targetVisited[nodeId].level;
if (degree <= MaxDegree)
return BuildResult(source, target, sourceVisited, targetVisited, nodeId);
}
}
}
if (sourceLevel >= MaxDegree && targetLevel >= MaxDegree)
break;
}
return new DegreeResult { Degree = -1, Path = new List<Guid>() };
}
private async Task<List<Guid>> ExpandLevelAsync(
Queue<Guid> queue, Dictionary<Guid, (Guid? parent, int level)> visited, int level)
{
var currentLevelNodes = new List<Guid>();
int count = Math.Min(queue.Count, MaxNodesPerLevel);
for (int i = 0; i < count; i++)
{
var nodeId = queue.Dequeue();
currentLevelNodes.Add(nodeId);
}
var neighbors = await _graphService.GetNeighborsAsync(currentLevelNodes);
var newNodes = new List<Guid>();
foreach (var (nodeId, neighborIds) in neighbors)
{
foreach (var neighborId in neighborIds)
{
if (!visited.ContainsKey(neighborId))
{
visited[neighborId] = (nodeId, level);
queue.Enqueue(neighborId);
newNodes.Add(neighborId);
}
}
}
return newNodes;
}
private DegreeResult BuildResult(
Guid source, Guid target,
Dictionary<Guid, (Guid? parent, int level)> sourceVisited,
Dictionary<Guid, (Guid? parent, int level)> targetVisited,
Guid meetingNode)
{
var path = new List<Guid>();
var current = (Guid?)meetingNode;
while (current.HasValue)
{
path.Add(current.Value);
current = sourceVisited[current.Value].parent;
}
path.Reverse();
current = targetVisited[meetingNode].parent;
while (current.HasValue)
{
path.Add(current.Value);
current = targetVisited[current.Value].parent;
}
int degree = path.Count - 1;
return new DegreeResult { Degree = degree, Path = path };
}
}
public class DegreeResult
{
public int Degree { get; set; }
public List<Guid> Path { get; set; } = new();
}
Connection Request Flow
8. News Feed Generation
LinkedIn's news feed is one of the most complex real-time systems in the world, generating over 40 billion impressions daily. The feed must balance relevance, timeliness, and diversity while respecting the user's professional context.
Feed Architecture: Fanout-on-Write
LinkedIn uses a hybrid fanout model. For users with fewer than 10,000 connections, posts are pre-computed and stored in the user's feed cache (fanout-on-write). For users with millions of followers (celebrity accounts, major companies), the feed is computed at read time (fanout-on-read) to avoid write amplification.
ML Feed Ranking Pipeline
The feed ranking is a multi-stage ML pipeline. The first stage generates candidate posts (hundreds), the second stage scores them with a lightweight model, and the third stage applies a heavy neural network for final ranking. Features include relationship strength, content similarity, engagement probability, recency, and professional relevance.
public class FeedRankingService
{
private readonly IFeedCandidateGenerator _candidateGenerator;
private readonly IMLRankingModel _rankingModel;
private readonly IDiversityInjector _diversityInjector;
private readonly IEngagementPredictor _engagementPredictor;
public FeedRankingService(
IFeedCandidateGenerator candidateGenerator,
IMLRankingModel rankingModel,
IDiversityInjector diversityInjector,
IEngagementPredictor engagementPredictor)
{
_candidateGenerator = candidateGenerator;
_rankingModel = rankingModel;
_diversityInjector = diversityInjector;
_engagementPredictor = engagementPredictor;
}
public async Task<List<FeedItem>> RankFeedAsync(Guid userId, FeedRequest request)
{
var candidates = await _candidateGenerator.GenerateCandidatesAsync(userId, maxCandidates: 500);
var scoredItems = new List<ScoredFeedItem>();
foreach (var candidate in candidates)
{
var features = await ExtractFeaturesAsync(userId, candidate);
var score = await _rankingModel.PredictScoreAsync(features);
scoredItems.Add(new ScoredFeedItem
{
Item = candidate,
Score = score,
Features = features
});
}
scoredItems = scoredItems
.OrderByDescending(s => s.Score)
.Take(200)
.ToList();
var diversifiedItems = _diversityInjector.InjectDiversity(scoredItems, new DiversityConstraints
{
MaxConsecutiveSameAuthor = 2,
MinContentTypeSpread = 0.3,
MaxSponsoredRatio = 0.15,
EnsureJobContent = true,
EnsureIndustryNews = true
});
var finalFeed = diversifiedItems
.Take(request.PageSize)
.Select(s => s.Item)
.ToList();
return finalFeed;
}
private async Task<Dictionary<string, float>> ExtractFeaturesAsync(Guid userId, FeedItem candidate)
{
var engagement = await _engagementPredictor.PredictAsync(userId, candidate);
return new Dictionary<string, float>
{
["relationship_strength"] = engagement.RelationshipStrength,
["content_relevance"] = engagement.ContentRelevance,
["author_engagement_rate"] = engagement.AuthorEngagementRate,
["recency_score"] = CalculateRecencyScore(candidate.CreatedAt),
["engagement_velocity"] = engagement.EngagementVelocity,
["topic_affinity"] = engagement.TopicAffinity,
["format_preference"] = engagement.FormatPreference,
["session_freshness"] = engagement.SessionFreshness,
["sponsored_score"] = candidate.IsSponsored ? 0.8f : 0f,
["dwell_time_prediction"] = engagement.PredictedDwellTimeSeconds
};
}
private float CalculateRecencyScore(DateTime createdAt)
{
var age = DateTime.UtcNow - createdAt;
if (age.TotalHours < 1) return 1.0f;
if (age.TotalHours < 6) return 0.9f;
if (age.TotalHours < 24) return 0.7f;
if (age.TotalDays < 3) return 0.4f;
return Math.Max(0.1f, 1.0f - (float)(age.TotalDays / 30.0));
}
}
public class FeedItem
{
public Guid PostId { get; set; }
public Guid AuthorId { get; set; }
public string AuthorName { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public List<string> MediaUrls { get; set; } = new();
public PostType Type { get; set; }
public DateTime CreatedAt { get; set; }
public int LikeCount { get; set; }
public int CommentCount { get; set; }
public int ShareCount { get; set; }
public bool IsSponsored { get; set; }
}
public class ScoredFeedItem
{
public FeedItem Item { get; set; } = null!;
public float Score { get; set; }
public Dictionary<string, float> Features { get; set; } = new();
}
public enum PostType
{
TextOnly,
Image,
Video,
Article,
DocumentCarousel,
Poll,
JobUpdate,
ArticleShare,
Repost
}
9. Job Matching & Recommendation
LinkedIn's job marketplace is one of the most sophisticated matching engines in the world, connecting over 20 million active job listings with 61 million job seekers daily. The system uses a dual-sided recommendation approach: suggesting jobs to candidates and suggesting candidates to recruiters.
Job Matching Pipeline
Matching Score Components
| Feature | Weight | Description |
|---|---|---|
| Skills Match | 0.30 | Overlap between candidate skills and job requirements (NLP extracted) |
| Experience Level | 0.20 | Years of experience and seniority alignment |
| Education Match | 0.10 | Degree level and field relevance |
| Location Preference | 0.15 | Geographic compatibility, remote/on-site preference |
| Salary Alignment | 0.10 | Expected vs. offered compensation range overlap |
| Industry Fit | 0.05 | Industry background relevance |
| Company Interest | 0.05 | Historical interaction with the company (views, follows) |
| Apply Probability | 0.05 | ML-predicted probability of application given impression |
10. Profile System & Endorsements
LinkedIn profiles serve as the universal professional identity layer. Each profile is a rich document containing structured data (skills, experience, education) and unstructured content (summary, articles, recommendations). The profile system must support complex queries like "find software engineers with Python skills in San Francisco who work at FAANG companies."
Profile Data Architecture
Profile data is stored in a relational database (MySQL cluster) for transactional consistency, with search indexes maintained in Elasticsearch for discovery. Profile photos and documents are stored in object storage (S3) behind a CDN. A read-optimized cache layer (Redis) stores frequently accessed profile fragments for hot profiles.
Endorsement System
The endorsement system allows 1st-degree connections to validate specific skills on a profile. Endorsements are weighted by the endorser's own expertise in that skill, the strength of the relationship, and the recency of interaction. The endorsement count for each skill feeds into search ranking and job matching algorithms.
public class EndorsementService
{
private readonly IEndorsementRepository _endorsementRepo;
private readonly IProfileRepository _profileRepo;
private readonly IGraphService _graphService;
private readonly IEventPublisher _eventPublisher;
public async Task<EndorsementResult> EndorseSkillAsync(
Guid endorserId, Guid endorsedUserId, string skillName)
{
if (endorserId == endorsedUserId)
return EndorsementResult.Failed("Cannot endorse yourself");
var areConnected = await _graphService.AreConnectedAsync(endorserId, endorsedUserId);
if (!areConnected)
return EndorsementResult.Failed("Must be 1st-degree connections");
var existing = await _endorsementRepo.GetEndorsementAsync(endorserId, endorsedUserId, skillName);
if (existing != null)
return EndorsementResult.Failed("Already endorsed this skill");
var endorserProfile = await _profileRepo.GetProfileAsync(endorserId);
var expertiseScore = CalculateExpertiseScore(endorserProfile, skillName);
var relationshipStrength = await _graphService.GetRelationshipStrengthAsync(endorserId, endorsedUserId);
var endorsement = new Endorsement
{
Id = Guid.NewGuid(),
EndorserId = endorserId,
EndorsedUserId = endorsedUserId,
SkillName = skillName,
ExpertiseWeight = expertiseScore,
RelationshipWeight = relationshipStrength,
CompositeWeight = expertiseScore * 0.6f + relationshipStrength * 0.4f,
CreatedAt = DateTime.UtcNow
};
await _endorsementRepo.SaveEndorsementAsync(endorsement);
await _profileRepo.IncrementSkillEndorsementAsync(endorsedUserId, skillName, endorsement.CompositeWeight);
await _eventPublisher.PublishAsync(new SkillEndorsedEvent(endorsement));
return EndorsementResult.Success(endorsement);
}
private float CalculateExpertiseScore(UserProfile endorser, string skillName)
{
var skill = endorser.Skills.FirstOrDefault(s =>
s.Name.Equals(skillName, StringComparison.OrdinalIgnoreCase));
if (skill == null) return 0.3f;
float baseScore = Math.Min(1.0f, skill.EndorsementCount / 99.0f);
bool hasExperience = endorser.Experience.Any(e =>
e.Description.Contains(skillName, StringComparison.OrdinalIgnoreCase) ||
e.Title.Contains(skillName, StringComparison.OrdinalIgnoreCase));
return hasExperience ? Math.Min(1.0f, baseScore + 0.3f) : baseScore;
}
}
public class Endorsement
{
public Guid Id { get; set; }
public Guid EndorserId { get; set; }
public Guid EndorsedUserId { get; set; }
public string SkillName { get; set; } = string.Empty;
public float ExpertiseWeight { get; set; }
public float RelationshipWeight { get; set; }
public float CompositeWeight { get; set; }
public DateTime CreatedAt { get; set; }
}
public class EndorsementResult
{
public bool IsSuccess { get; set; }
public string? ErrorMessage { get; set; }
public Endorsement? Endorsement { get; set; }
public static EndorsementResult Success(Endorsement e) =>
new() { IsSuccess = true, Endorsement = e };
public static EndorsementResult Failed(string msg) =>
new() { IsSuccess = false, ErrorMessage = msg };
}
11. Messaging & InMail
LinkedIn's messaging system handles over 10 billion messages per month, supporting one-on-one conversations, group chats, and InMail (premium messaging to non-connections). The system must ensure message ordering, delivery guarantees, and real-time presence updates.
Messaging Architecture
Messages are stored in Cassandra with conversation_id as the partition key, ensuring all messages within a conversation reside on the same partition for efficient range queries. Each message is assigned a timestamp-based sequence number that guarantees total ordering within a conversation. WebSocket connections maintain real-time message delivery to online users, while push notifications and emails handle offline delivery.
InMail System
InMail allows premium members to message anyone on LinkedIn, even without a connection. InMail credits are limited (varies by subscription tier), and the system enforces delivery-read guarantees: an InMail is only deducted from the sender's balance when the recipient opens or reads the message. This requires a two-phase credit system with expiry and refund handling.
12. Company Pages & Employer Branding
Company pages serve as the hub for employer branding, with over 65 million companies maintaining pages on LinkedIn. Each page supports multiple administrators, branded content, job listings, follower management, and analytics dashboards.
Company Page Data Model
| Entity | Key Fields | Relationships |
|---|---|---|
| company_pages | company_id, name, industry, size, logo, description, website, headquarters | Has many administrators, followers, jobs, posts |
| company_admins | user_id, company_id, role (admin/super_admin/content_admin), permissions | Many-to-one with companies and users |
| company_followers | user_id, company_id, followed_at, notification_preference | Many-to-many between users and companies |
| company_insights | company_id, date, follower_count, page_views, unique_visitors, job_clicks | Time-series data partitioned by company_id and month |
| company_updates | update_id, company_id, content, media, posted_by, created_at | Company posts appearing in follower feeds |
Company page analytics are computed as a batch pipeline. Raw events (page views, follower actions, job clicks) flow through Kafka into a data warehouse (Vertica/Presto), where aggregations are computed hourly and materialized into the insights table. Admin dashboards then query these pre-computed aggregates for near-real-time reporting.
13. Content Publishing Platform
LinkedIn's content publishing platform supports articles (long-form), posts (short-form with rich media), document carousels (PDFs), polls, newsletters, and live video. The platform generates massive write traffic (500M+ posts daily) and even larger read traffic through the feed, notifications, and search.
Content Processing Pipeline
Content moderation uses a combination of automated AI classifiers (for spam, inappropriate content, misinformation) and human review queues for borderline cases. Articles go through an additional quality scoring step that predicts engagement potential, which feeds into the feed ranking algorithm.
14. Search System
LinkedIn's search system indexes over 1 billion profiles, 20 million job listings, 65 million company pages, and billions of posts. It supports faceted search with filters (location, company, skills, experience level), autocomplete, and personalized ranking.
Search Architecture
LinkedIn uses a multi-cluster Elasticsearch deployment with dedicated clusters for people search, job search, company search, and content search. Each cluster is independently sized based on the query patterns and document volumes of that domain. People search is the most demanding, handling approximately 50% of all search traffic.
People Search Ranking Features
| Feature | Type | Impact |
|---|---|---|
| Keyword Match Score | Text | BM25 relevance of query terms against profile fields |
| Profile Completeness | Profile | Complete profiles ranked higher (premium signal) |
| Connection Degree | Graph | 1st-degree connections boosted in results |
| Mutual Connections | Graph | More mutual connections = higher rank |
| Engagement Recency | Behavioral | Recently active profiles ranked higher |
| Query-Click Feedback | Behavioral | Profiles frequently clicked for similar queries boosted |
| Premium Status | Monetization | Premium subscribers get slight ranking boost |
| Hiring Intent | Behavioral | Users actively seeking jobs get boosted for recruiter queries |
15. Notification System
LinkedIn sends over 30 billion notifications per day across push (mobile), email, and in-app channels. The notification system must handle massive fanout, personalization, rate limiting, and digest aggregation to avoid notification fatigue.
Notification Pipeline
Notifications are generated from events via Kafka consumers, processed through a rules engine that determines relevance, timing, and channel, and then dispatched to the appropriate delivery infrastructure. The system supports batched digests (daily/weekly email summaries), real-time push notifications, and quiet hours based on user preferences and timezone.
16. Recruiter Tools & Pipeline
LinkedIn Recruiter is a premium product used by over 400,000 recruiters worldwide. It provides advanced candidate search, pipeline management, team collaboration, and outreach automation. The Recruiter system must efficiently search across 1B+ profiles with complex boolean queries while maintaining strict privacy boundaries.
Recruiter Search vs. Regular Search
Recruiter search has access to additional profile data points that regular search does not, including private profile information (openness to work, salary expectations, detailed activity logs). The search index for Recruiter is maintained separately with additional fields and stricter access controls. Every Recruiter search is audited for compliance with LinkedIn's privacy policies and applicable employment laws.
Pipeline Management
Recruiters can organize candidates into stages (sourced, contacted, interviewing, offered, hired) with notes, tags, and team collaboration features. The pipeline is essentially a CRM built on top of the social graph, with real-time updates synced across team members via WebSocket connections.
17. Learning Platform (LinkedIn Learning)
LinkedIn Learning offers 21,000+ courses across business, technology, and creative topics, with over 27 million learners. The platform includes video content delivery (with adaptive bitrate streaming), skill assessments, learning paths, certificates, and integration with the profile's skills section.
Learning System Architecture
| Component | Technology | Scale |
|---|---|---|
| Video Storage | Object storage (S3) + CDN | 500TB+ of video content |
| Video Streaming | HLS/DASH with adaptive bitrate | 10M+ concurrent streams at peak |
| Course Metadata | MySQL + Elasticsearch | 21K courses, 200K+ videos |
| Progress Tracking | Cassandra (time-series) | Billions of progress events |
| Assessments | Question bank + proctoring service | 50+ skill assessments |
| Recommendations | ML pipeline (skill gap analysis) | Personalized per user profile |
The skill assessment feature evaluates a member's proficiency in a specific skill and awards a badge on their profile. Assessment questions are drawn from a calibrated question bank, with adaptive difficulty based on response accuracy. Results feed into the job matching algorithm, giving assessed users higher match scores for relevant positions.
18. Ads & Sponsored Content
LinkedIn's advertising platform generated over $15 billion in annual revenue in 2025, making it the dominant B2B advertising platform globally. The ad system supports sponsored content, message ads (InMail ads), dynamic ads, text ads, and event ads, all with sophisticated targeting based on professional attributes.
Ad Targeting Dimensions
| Dimension | Examples | Granularity |
|---|---|---|
| Job Title | Software Engineer, VP of Marketing, CEO | Individual titles + title categories |
| Company | Specific companies, company size, industry | Exact match + category |
| Skills | Python, Machine Learning, Project Management | Individual skills + skill clusters |
| Education | University, degree, field of study | Exact + category |
| Location | Country, state, city, metro area | Geographic hierarchy |
| Industry | Technology, Healthcare, Finance | 2-digit and 4-digit NAICS codes |
| Experience Level | Entry, Senior, Director, VP, C-Suite | Seniority levels |
| Interests | LinkedIn Learning topics, groups joined | Topic taxonomy |
The ad auction system uses a second-price auction model with adjustments for ad quality (predicted click-through rate and relevance score). The ML models predicting ad engagement are retrained daily on impression, click, and conversion data. Ad delivery is governed by budget pacing algorithms that distribute spend evenly over the campaign's lifetime.
19. Database Sharding Strategy
With petabytes of data, horizontal sharding is essential for LinkedIn's databases. The sharding strategy varies by data type based on access patterns.
Sharding Approaches
| Data Type | Shard Key | Strategy | Replicas |
|---|---|---|---|
| Users/Profiles | user_id (hash) | Consistent hashing across 1024 shards | 3 per shard |
| Connections | user_id (hash) | Co-located with user profile shard | 3 per shard |
| Messages | conversation_id (hash) | Co-locate conversation messages | 3 per shard |
| Posts | author_id (hash) | Co-located with author profile | 3 per shard |
| Job Listings | company_id (hash) | Co-locate with company page | 3 per shard |
| Notifications | user_id (hash) | Co-located with user profile | 3 per shard |
| Feed Cache | user_id (hash) | Redis cluster with consistent hashing | 2 per shard |
20. Caching Strategy
LinkedIn employs a multi-layered caching architecture with different TTLs and eviction strategies for each data type.
Cache Hierarchy
| Layer | Technology | Data Types | TTL | Hit Rate Target |
|---|---|---|---|---|
| L1 - Browser | Service Worker + IndexedDB | Profile fragments, feed items | 5-30 min | 40% |
| L2 - CDN | Akamai/CloudFront | Static assets, profile photos, media | 24 hours | 85% |
| L3 - Application | Redis Cluster (in-memory) | Feed cache, session data, counters | 1-60 min | 92% |
| L4 - Database | MySQL buffer pool + query cache | Hot profile data, recent messages | N/A (DB managed) | 95% |
Cache invalidation follows a write-through pattern for critical data (profile updates, connection changes) and write-behind for eventual-consistency data (feed counts, like counts). The cache stampede problem is mitigated using probabilistic early expiration (Lock-based approach with mutex for cold keys).
21. Multi-Region Design
LinkedIn operates in multiple AWS regions globally: US-East, US-West, EU-West, APAC-Southeast, and APAC-Northeast. Each region serves traffic for nearby users, with cross-region replication for critical data.
Conflict Resolution: For profile updates arriving from different regions, LinkedIn uses a last-writer-wins (LWW) strategy based on vector clocks. For critical operations like job applications and messages, writes are routed to the primary region via a global coordination service to ensure strong consistency.
22. Cost Estimation
| Component | Configuration | Monthly Cost (Est.) |
|---|---|---|
| Application Servers | 5000 instances (m5.2xlarge equivalent) | $1,500,000 |
| MySQL Cluster | 100 shards × 3 replicas (r5.4xlarge) | $1,200,000 |
| Cassandra Cluster | 2000 nodes (i3.2xlarge) | $1,000,000 |
| Redis Cluster | 500 nodes (r5.xlarge) | $300,000 |
| Elasticsearch | 300 data nodes (r5.2xlarge) | $500,000 |
| Kafka Cluster | 200 brokers (kafka.m5.2xlarge) | $250,000 |
| Object Storage (S3) | 5 PB stored + transfers | $200,000 |
| CDN | 100 PB/month transfer | $800,000 |
| ML/GPU Infrastructure | 200 GPU instances (p3.2xlarge) | $600,000 |
| Networking & Data Transfer | Cross-region + internet egress | $400,000 |
| Monitoring & Observability | Datadog/Splunk equivalent | $200,000 |
| Engineering Team (200 engineers) | Salaries, tools, offices | $10,000,000 |
| Total Monthly Infrastructure | ~$6,950,000 | |
| Total Monthly (incl. team) | ~$16,950,000 |
23. Interview Q&A (10+ Questions)
Q1: How does LinkedIn store and traverse the connection graph at scale?
Answer: LinkedIn uses a custom graph storage system (Terminus/SGS) that stores the social graph as an adjacency list, partitioned across thousands of machines by node ID. Each partition stores a subset of nodes and their complete adjacency lists. For degree-of-separation queries, bi-directional BFS is used with a maximum depth of 3 degrees. Cross-partition edges are resolved via asynchronous lookups. The graph is replicated 3x for fault tolerance and cached in Redis for frequently accessed subgraphs.
Q2: Explain the fanout-on-write vs. fanout-on-read trade-off for the news feed.
Answer: Fanout-on-write pre-computes each user's feed when a post is created, writing to all followers' feed caches. This gives O(1) read latency but causes write amplification for users with many followers. Fanout-on-read computes the feed at request time by merging posts from followed users. LinkedIn uses a hybrid approach: fanout-on-write for regular users (fewer than 10K connections) and fanout-on-read for celebrity accounts (10K+ followers) to balance write amplification against read latency.
Q3: How would you design the messaging system to ensure message ordering and delivery?
Answer: Each conversation has a monotonically increasing sequence number assigned by a centralized sequence service (or partition-local counter using Kafka partition key = conversation_id). Messages are stored in Cassandra partitioned by conversation_id, which guarantees ordering within a partition. For delivery, WebSocket connections provide real-time push to online users, while a persistent queue handles offline delivery via push notifications and email. Read receipts are tracked separately with a lightweight acknowledgment table.
Q4: How does LinkedIn's job matching algorithm work?
Answer: LinkedIn's job matching uses a two-tower neural network model: one tower encodes the job listing (requirements, title, company, location) and the other encodes the candidate profile (skills, experience, preferences). The dot product of the two embeddings predicts match quality. The model is trained on historical application and engagement data. At serving time, approximate nearest neighbor (ANN) search retrieves top candidates for a job or top jobs for a candidate, which are then re-ranked by a gradient-boosted model incorporating additional features.
Q5: How would you handle cache stampede for a viral LinkedIn post?
Answer: When a post goes viral, millions of users may try to view it simultaneously, causing a cache stampede. The mitigation strategy has three layers: (1) probabilistic early expiration — cache entries are refreshed before TTL expires with increasing probability, (2) mutex-based regeneration — only one thread regenerates the cache entry while others wait or serve stale data, (3) request coalescing — multiple concurrent requests for the same key are batched into a single database query. For extremely viral content, the post is pinned into a dedicated hot cache tier with no TTL.
Q6: Design the notification system to handle 30 billion notifications per day without overwhelming users.
Answer: The notification system uses a scoring model to predict engagement probability for each notification. Notifications below a threshold are suppressed or batched into digests. The system supports channel preferences (push, email, in-app), quiet hours based on timezone, and notification bundling (e.g., "5 people liked your post" instead of 5 separate notifications). Rate limiting is enforced per user (max 50 real-time push notifications per day). The pipeline processes events from Kafka through a rules engine that applies deduplication, batching, and timing optimization before dispatching to delivery channels.
Q7: How would you design a search system that handles autocomplete with sub-100ms latency?
Answer: Autocomplete requires a specialized index structure different from full-text search. LinkedIn uses a trie-based index stored in memory (or backed by a fast KV store). The top suggestions for each prefix are precomputed and cached. The trie is updated incrementally as new profiles, companies, and jobs are created. For personalization, the top suggestions are re-ranked based on the user's search history and professional context. The entire trie for popular prefixes fits in memory across a Redis cluster, enabling sub-10ms response times.
Q8: Explain how you would shard the messages database for 10 billion messages per month.
Answer: Messages are sharded by conversation_id using consistent hashing. This ensures all messages in a conversation are on the same partition, enabling efficient range scans. The partition key is the conversation_id (UUID hashed). For conversations with very high volume (group chats with thousands of members), we use a secondary partition on message_id time buckets (monthly partitions within each conversation shard). Hot conversations are detected via access pattern monitoring and migrated to dedicated shards. Data is retained in Cassandra with a TTL-based compaction strategy, archiving messages older than 2 years to cold storage.
Q9: How would you ensure data consistency between the connection graph and the feed fanout system?
Answer: This is a classic dual-write consistency problem. When a connection is accepted, the graph service emits a ConnectionAccepted event to Kafka. The feed fanout service consumes this event and updates the pre-computed feed caches. If the fanout fails, an outbox pattern ensures at-least-once delivery: the connection state change is written to an outbox table in the same transaction, and a separate process polls the outbox and publishes to Kafka. Consumers are idempotent, using event IDs to prevent duplicate processing. The eventual consistency window is typically under 5 seconds.
Q10: How would you design the "Who Viewed Your Profile" feature at scale?
Answer: Every profile view generates a view event stored in a time-series table (Cassandra, partitioned by viewed_user_id, clustered by timestamp). For free users, the system shows only the count and aggregated demographics (company, job title, location). Premium users see the full viewer list with a 90-day retention window. The view counter is maintained in Redis for real-time updates and periodically flushed to the database. Privacy rules are enforced: anonymous viewers (who opted out) are stored with a placeholder identity, and the viewer list is only visible to the profile owner.
Q11: How would you handle a situation where LinkedIn needs to migrate from one database technology to another without downtime?
Answer: LinkedIn would use the Strangler Fig pattern with dual-write and backfill. During migration: (1) Both old and new databases receive writes simultaneously via a write proxy layer. (2) Historical data is backfilled from old to new database via a batch pipeline. (3) Reads are gradually shifted from old to new via feature flags, starting with 1% of traffic and ramping up. (4) Read verification compares results from both databases to detect discrepancies. (5) Once confidence is established, the old database is decommissioned. The entire migration can take weeks to months for critical systems, with rollback capability at each stage.
24. Full C# Implementation (300+ Lines)
Below is a comprehensive C# implementation of a LinkedIn-like connection and feed system, demonstrating the core architectural patterns discussed throughout this article. This implementation includes the connection graph, feed generation, job matching, endorsement system, and messaging — all production-ready patterns suitable for a senior-level system design discussion.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace LinkedInSystemDesign
{
// ============================================================
// DOMAIN MODELS
// ============================================================
public enum ConnectionStatus { Pending, Accepted, Rejected, Blocked }
public enum NotificationType { ConnectionRequest, ConnectionAccepted, JobRecommendation, MessageReceived, Endorsement, PostEngagement }
public record User(Guid Id, string Name, string Email, string Headline, string Industry, List<string> Skills);
public record Company(Guid Id, string Name, string Industry, int EmployeeCount);
public record JobListing(Guid Id, Guid CompanyId, string Title, string Description, List<string> RequiredSkills, string Location, decimal MinSalary, decimal MaxSalary);
public class Connection
{
public Guid Id { get; init; } = Guid.NewGuid();
public Guid SenderId { get; init; }
public Guid ReceiverId { get; init; }
public ConnectionStatus Status { get; set; } = ConnectionStatus.Pending;
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
public DateTime? AcceptedAt { get; set; }
}
public class Post
{
public Guid Id { get; init; } = Guid.NewGuid();
public Guid AuthorId { get; init; }
public string Content { get; init; } = string.Empty;
public List<string> MediaUrls { get; init; } = new();
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
public int LikeCount { get; set; }
public int CommentCount { get; set; }
public int ShareCount { get; set; }
}
public class Message
{
public Guid Id { get; init; } = Guid.NewGuid();
public Guid ConversationId { get; init; }
public Guid SenderId { get; init; }
public string Body { get; init; } = string.Empty;
public DateTime SentAt { get; init; } = DateTime.UtcNow;
public bool IsRead { get; set; }
}
public class Conversation
{
public Guid Id { get; init; } = Guid.NewGuid();
public List<Guid> ParticipantIds { get; init; } = new();
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
public DateTime LastMessageAt { get; set; }
}
public class Notification
{
public Guid Id { get; init; } = Guid.NewGuid();
public Guid UserId { get; init; }
public NotificationType Type { get; init; }
public string Message { get; init; } = string.Empty;
public Dictionary<string, string> Metadata { get; init; } = new();
public bool IsRead { get; set; }
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
}
public class FeedItem
{
public Post Post { get; init; } = null!;
public float Score { get; set; }
public string AuthorName { get; init; } = string.Empty;
}
// ============================================================
// REPOSITORY INTERFACES (Repository Pattern)
// ============================================================
public interface IUserRepository
{
Task<User?> GetByIdAsync(Guid id);
Task<List<User>> GetByIdsAsync(IEnumerable<Guid> ids);
Task<List<User>> SearchAsync(string query, int limit);
}
public interface IConnectionRepository
{
Task<Connection?> GetAsync(Guid senderId, Guid receiverId);
Task<List<Connection>> GetByUserIdAsync(Guid userId);
Task<List<Guid>> GetConnectionIdsAsync(Guid userId);
Task<Connection> SaveAsync(Connection connection);
}
public interface IPostRepository
{
Task<Post> SaveAsync(Post post);
Task<Post?> GetByIdAsync(Guid postId);
Task<List<Post>> GetByAuthorIdAsync(Guid authorId, int limit);
}
public interface IMessageRepository
{
Task<Conversation> GetOrCreateConversationAsync(List<Guid> participantIds);
Task<Message> SaveMessageAsync(Message message);
Task<List<Message>> GetMessagesAsync(Guid conversationId, int limit, Guid? beforeMessageId);
Task<List<Conversation>> GetUserConversationsAsync(Guid userId);
}
public interface IJobRepository
{
Task<List<JobListing>> SearchAsync(string query, string? location, int limit);
Task<JobListing?> GetByIdAsync(Guid jobId);
}
public interface IFeedCache
{
Task<List<FeedItem>> GetFeedAsync(Guid userId, int offset, int limit);
Task<int> AddToFeedAsync(Guid userId, FeedItem item);
Task<int> InvalidateFeedAsync(Guid userId);
}
// ============================================================
// IN-MEMORY REPOSITORY IMPLEMENTATIONS (For Demo)
// ============================================================
public class InMemoryUserRepository : IUserRepository
{
private readonly ConcurrentDictionary<Guid, User> _users = new();
public void AddUser(User user) => _users[user.Id] = user;
public Task<User?> GetByIdAsync(Guid id) =>
Task.FromResult(_users.TryGetValue(id, out var user) ? user : null);
public Task<List<User>> GetByIdsAsync(IEnumerable<Guid> ids) =>
Task.FromResult(ids.Where(id => _users.ContainsKey(id)).Select(id => _users[id]).ToList());
public Task<List<User>> SearchAsync(string query, int limit) =>
Task.FromResult(_users.Values
.Where(u => u.Name.Contains(query, StringComparison.OrdinalIgnoreCase) ||
u.Headline.Contains(query, StringComparison.OrdinalIgnoreCase))
.Take(limit).ToList());
}
public class InMemoryConnectionRepository : IConnectionRepository
{
private readonly ConcurrentDictionary<string, Connection> _connections = new();
private readonly ConcurrentDictionary<Guid, List<Guid>> _adjacencyList = new();
public Task<Connection?> GetAsync(Guid senderId, Guid receiverId)
{
var key = $"{senderId}:{receiverId}";
_connections.TryGetValue(key, out var conn);
return Task.FromResult(conn);
}
public Task<List<Connection>> GetByUserIdAsync(Guid userId)
{
var conns = _connections.Values
.Where(c => c.SenderId == userId || c.ReceiverId == userId)
.ToList();
return Task.FromResult(conns);
}
public Task<List<Guid>> GetConnectionIdsAsync(Guid userId)
{
_adjacencyList.TryGetValue(userId, out var neighbors);
return Task.FromResult(neighbors?.ToList() ?? new List<Guid>());
}
public Task<Connection> SaveAsync(Connection connection)
{
var key = $"{connection.SenderId}:{connection.ReceiverId}";
_connections[key] = connection;
_adjacencyList.AddOrUpdate(connection.SenderId,
_ => new List<Guid> { connection.ReceiverId },
(_, list) => { lock (list) { list.Add(connection.ReceiverId); } return list; });
_adjacencyList.AddOrUpdate(connection.ReceiverId,
_ => new List<Guid> { connection.SenderId },
(_, list) => { lock (list) { list.Add(connection.SenderId); } return list; });
return Task.FromResult(connection);
}
public List<Guid> GetAllNeighbors(Guid userId)
{
_adjacencyList.TryGetValue(userId, out var neighbors);
return neighbors?.ToList() ?? new List<Guid>();
}
}
public class InMemoryPostRepository : IPostRepository
{
private readonly ConcurrentDictionary<Guid, Post> _posts = new();
public Task<Post> SaveAsync(Post post)
{
_posts[post.Id] = post;
return Task.FromResult(post);
}
public Task<Post?> GetByIdAsync(Guid postId)
{
_posts.TryGetValue(postId, out var post);
return Task.FromResult(post);
}
public Task<List<Post>> GetByAuthorIdAsync(Guid authorId, int limit)
{
var posts = _posts.Values
.Where(p => p.AuthorId == authorId)
.OrderByDescending(p => p.CreatedAt)
.Take(limit).ToList();
return Task.FromResult(posts);
}
}
public class InMemoryFeedCache : IFeedCache
{
private readonly ConcurrentDictionary<Guid, List<FeedItem>> _feedCache = new();
public Task<List<FeedItem>> GetFeedAsync(Guid userId, int offset, int limit)
{
_feedCache.TryGetValue(userId, out var feed);
var items = feed?.Skip(offset).Take(limit).ToList() ?? new List<FeedItem>();
return Task.FromResult(items);
}
public Task<int> AddToFeedAsync(Guid userId, FeedItem item)
{
_feedCache.AddOrUpdate(userId,
_ => new List<FeedItem> { item },
(_, list) => { lock (list) { list.Add(item); } return list; });
return Task.FromResult(1);
}
public Task<int> InvalidateFeedAsync(Guid userId)
{
_feedCache.TryRemove(userId, out _);
return Task.FromResult(1);
}
}
public class InMemoryJobRepository : IJobRepository
{
private readonly ConcurrentDictionary<Guid, JobListing> _jobs = new();
public void AddJob(JobListing job) => _jobs[job.Id] = job;
public Task<List<JobListing>> SearchAsync(string query, string? location, int limit)
{
var results = _jobs.Values
.Where(j => j.Title.Contains(query, StringComparison.OrdinalIgnoreCase) ||
j.Description.Contains(query, StringComparison.OrdinalIgnoreCase))
.Where(j => location == null || j.Location.Contains(location, StringComparison.OrdinalIgnoreCase))
.Take(limit).ToList();
return Task.FromResult(results);
}
public Task<JobListing?> GetByIdAsync(Guid jobId)
{
_jobs.TryGetValue(jobId, out var job);
return Task.FromResult(job);
}
}
// ============================================================
// CORE SERVICES
// ============================================================
public class ConnectionService
{
private readonly IConnectionRepository _connectionRepo;
private readonly IFeedCache _feedCache;
private readonly List<Notification> _notifications = new();
public ConnectionService(IConnectionRepository connectionRepo, IFeedCache feedCache)
{
_connectionRepo = connectionRepo;
_feedCache = feedCache;
}
public async Task<Connection> SendRequestAsync(Guid senderId, Guid receiverId)
{
var existing = await _connectionRepo.GetAsync(senderId, receiverId);
if (existing != null)
throw new InvalidOperationException("Connection request already exists.");
var reverse = await _connectionRepo.GetAsync(receiverId, senderId);
if (reverse != null)
throw new InvalidOperationException("Already connected.");
var connection = new Connection { SenderId = senderId, ReceiverId = receiverId };
await _connectionRepo.SaveAsync(connection);
_notifications.Add(new Notification
{
UserId = receiverId,
Type = NotificationType.ConnectionRequest,
Message = $"New connection request received",
Metadata = new Dictionary<string, string> { ["senderId"] = senderId.ToString() }
});
return connection;
}
public async Task<Connection> AcceptRequestAsync(Guid connectionId, Guid acceptingUserId)
{
var connections = await _connectionRepo.GetByUserIdAsync(acceptingUserId);
var connection = connections.FirstOrDefault(c => c.Id == connectionId && c.ReceiverId == acceptingUserId)
?? throw new InvalidOperationException("Connection not found.");
connection.Status = ConnectionStatus.Accepted;
connection.AcceptedAt = DateTime.UtcNow;
await _connectionRepo.SaveAsync(connection);
_notifications.Add(new Notification
{
UserId = connection.SenderId,
Type = NotificationType.ConnectionAccepted,
Message = "Connection request accepted",
Metadata = new Dictionary<string, string> { ["receiverId"] = acceptingUserId.ToString() }
});
return connection;
}
public async Task<int> GetDegreeOfSeparationAsync(Guid userId, Guid targetId)
{
if (userId == targetId) return 0;
var queue = new Queue<Guid>();
var visited = new Dictionary<Guid, int> { [userId] = 0 };
queue.Enqueue(userId);
while (queue.Count > 0)
{
var current = queue.Dequeue();
var currentDegree = visited[current];
if (currentDegree >= 3) continue;
var neighbors = await _connectionRepo.GetConnectionIdsAsync(current);
foreach (var neighbor in neighbors)
{
if (neighbor == targetId) return currentDegree + 1;
if (!visited.ContainsKey(neighbor))
{
visited[neighbor] = currentDegree + 1;
queue.Enqueue(neighbor);
}
}
}
return -1;
}
public List<Notification> GetPendingNotifications() =>
_notifications.Where(n => !n.IsRead).ToList();
}
public class FeedService
{
private readonly IConnectionRepository _connectionRepo;
private readonly IPostRepository _postRepo;
private readonly IUserRepository _userRepo;
private readonly IFeedCache _feedCache;
public FeedService(
IConnectionRepository connectionRepo,
IPostRepository postRepo,
IUserRepository userRepo,
IFeedCache feedCache)
{
_connectionRepo = connectionRepo;
_postRepo = postRepo;
_userRepo = userRepo;
_feedCache = feedCache;
}
public async Task<Post> CreatePostAsync(Post post)
{
await _postRepo.SaveAsync(post);
await FanoutPostAsync(post);
return post;
}
private async Task FanoutPostAsync(Post post)
{
var connectionIds = await _connectionRepo.GetConnectionIdsAsync(post.AuthorId);
var author = await _userRepo.GetByIdAsync(post.AuthorId);
if (connectionIds.Count < 10_000)
{
var feedItem = new FeedItem
{
Post = post,
Score = CalculateInitialScore(post),
AuthorName = author?.Name ?? "Unknown"
};
var fanoutTasks = connectionIds.Select(connId =>
_feedCache.AddToFeedAsync(connId, feedItem));
await Task.WhenAll(fanoutTasks);
}
}
public async Task<List<FeedItem>> GetFeedAsync(Guid userId, int page = 0, int pageSize = 20)
{
var cachedFeed = await _feedCache.GetFeedAsync(userId, page * pageSize, pageSize);
if (cachedFeed.Count > 0) return cachedFeed;
return await ComputeFeedOnReadAsync(userId, pageSize);
}
private async Task<List<FeedItem>> ComputeFeedOnReadAsync(Guid userId, int limit)
{
var connectionIds = await _connectionRepo.GetConnectionIdsAsync(userId);
var candidates = new List<FeedItem>();
foreach (var connId in connectionIds.Take(200))
{
var posts = await _postRepo.GetByAuthorIdAsync(connId, 5);
var user = await _userRepo.GetByIdAsync(connId);
foreach (var post in posts)
{
candidates.Add(new FeedItem
{
Post = post,
Score = CalculateFeedScore(userId, post, user),
AuthorName = user?.Name ?? "Unknown"
});
}
}
return candidates
.OrderByDescending(f => f.Score)
.Take(limit)
.ToList();
}
private float CalculateInitialScore(Post post) =>
1.0f / (1.0f + (float)(DateTime.UtcNow - post.CreatedAt).TotalHours);
private float CalculateFeedScore(Guid userId, Post post, User? author)
{
float recencyScore = 1.0f / (1.0f + (float)(DateTime.UtcNow - post.CreatedAt).TotalHours);
float engagementScore = (post.LikeCount + post.CommentCount * 2 + post.ShareCount * 3) / 100.0f;
float authorRelevance = author != null ? 0.5f : 0.1f;
return recencyScore * 0.4f + engagementScore * 0.4f + authorRelevance * 0.2f;
}
}
public class JobMatchingService
{
private readonly IJobRepository _jobRepo;
private readonly IUserRepository _userRepo;
private readonly IConnectionRepository _connectionRepo;
public JobMatchingService(IJobRepository jobRepo, IUserRepository userRepo, IConnectionRepository connectionRepo)
{
_jobRepo = jobRepo;
_userRepo = userRepo;
_connectionRepo = connectionRepo;
}
public async Task<List<ScoredJob>> GetJobRecommendationsAsync(Guid userId, int limit = 20)
{
var user = await _userRepo.GetByIdAsync(userId)
?? throw new InvalidOperationException("User not found.");
var allJobs = new List<JobListing>();
foreach (var skill in user.Skills)
{
var jobs = await _jobRepo.SearchAsync(skill, null, 50);
allJobs.AddRange(jobs);
}
var distinctJobs = allJobs.GroupBy(j => j.Id).Select(g => g.First()).ToList();
var scoredJobs = distinctJobs.Select(job => new ScoredJob
{
Job = job,
Score = CalculateJobMatchScore(user, job)
})
.OrderByDescending(sj => sj.Score)
.Take(limit)
.ToList();
return scoredJobs;
}
private float CalculateJobMatchScore(User user, JobListing job)
{
var matchingSkills = user.Skills
.Count(s => job.RequiredSkills.Any(rs =>
rs.Equals(s, StringComparison.OrdinalIgnoreCase)));
float skillsScore = job.RequiredSkills.Count > 0
? (float)matchingSkills / job.RequiredSkills.Count
: 0;
float industryScore = user.Industry.Equals(job.RequiredSkills.FirstOrDefault() ?? "", StringComparison.OrdinalIgnoreCase)
? 0.3f : 0.1f;
return skillsScore * 0.6f + industryScore * 0.2f + 0.2f;
}
}
public class EndorsementService
{
private readonly ConcurrentDictionary<string, int> _endorsements = new();
private readonly IConnectionRepository _connectionRepo;
public EndorsementService(IConnectionRepository connectionRepo)
{
_connectionRepo = connectionRepo;
}
public async Task<bool> EndorseSkillAsync(Guid endorserId, Guid endorsedUserId, string skillName)
{
var areConnected = (await _connectionRepo.GetConnectionIdsAsync(endorserId)).Contains(endorsedUserId);
if (!areConnected) return false;
var key = $"{endorsedUserId}:{skillName}";
_endorsements.AddOrUpdate(key, 1, (_, count) => count + 1);
return true;
}
public int GetEndorsementCount(Guid userId, string skillName)
{
var key = $"{userId}:{skillName}";
_endorsements.TryGetValue(key, out var count);
return count;
}
}
public class MessagingService
{
private readonly ConcurrentDictionary<Guid, Conversation> _conversations = new();
private readonly ConcurrentDictionary<Guid, List<Message>> _messages = new();
private long _sequenceCounter = 0;
public async Task<Message> SendMessageAsync(Guid senderId, Guid recipientId, string body)
{
var convKey = GetConversationKey(senderId, recipientId);
var conversation = _conversations.GetOrAdd(convKey, _ =>
new Conversation { ParticipantIds = new List<Guid> { senderId, recipientId } });
var message = new Message
{
ConversationId = conversation.Id,
SenderId = senderId,
Body = body
};
Interlocked.Increment(ref _sequenceCounter);
_messages.AddOrUpdate(conversation.Id,
_ => new List<Message> { message },
(_, list) => { lock (list) { list.Add(message); } return list; });
conversation.LastMessageAt = DateTime.UtcNow;
return await Task.FromResult(message);
}
public Task<List<Message>> GetConversationMessagesAsync(Guid conversationId, int limit = 50)
{
_messages.TryGetValue(conversationId, out var messages);
var result = messages?.OrderByDescending(m => m.SentAt).Take(limit).ToList()
?? new List<Message>();
return Task.FromResult(result);
}
private Guid GetConversationKey(Guid userA, Guid userB)
{
var sorted = new[] { userA, userB }.OrderBy(x => x).ToArray();
return DeterministicGuid.FromBytes(sorted[0].ToByteArray().Concat(sorted[1].ToByteArray()).ToArray());
}
}
public static class DeterministicGuid
{
public static Guid FromBytes(byte[] bytes)
{
var hash = System.Security.Cryptography.SHA256.HashData(bytes);
return new Guid(hash.Take(16).ToArray());
}
}
public class NotificationService
{
private readonly ConcurrentDictionary<Guid, List<Notification>> _notifications = new();
public void SendNotification(Notification notification)
{
_notifications.AddOrUpdate(notification.UserId,
_ => new List<Notification> { notification },
(_, list) => { lock (list) { list.Add(notification); } return list; });
}
public List<Notification> GetUserNotifications(Guid userId, bool unreadOnly = false)
{
_notifications.TryGetValue(userId, out var notifications);
if (notifications == null) return new List<Notification>();
var query = notifications.AsEnumerable();
if (unreadOnly) query = query.Where(n => !n.IsRead);
return query.OrderByDescending(n => n.CreatedAt).ToList();
}
public void MarkAsRead(Guid userId, Guid notificationId)
{
_notifications.TryGetValue(userId, out var notifications);
var notification = notifications?.FirstOrDefault(n => n.Id == notificationId);
if (notification != null) notification.IsRead = true;
}
}
// ============================================================
// COMPOSITE SCORED TYPES
// ============================================================
public class ScoredJob
{
public JobListing Job { get; init; } = null!;
public float Score { get; set; }
}
// ============================================================
// ORCHESTRATOR / DEMO
// ============================================================
public class LinkedInSystemDemo
{
public static async Task RunAsync()
{
Console.WriteLine("=== LinkedIn Professional Network System Design Demo ===\n");
var userRepo = new InMemoryUserRepository();
var connRepo = new InMemoryConnectionRepository();
var postRepo = new InMemoryPostRepository();
var feedCache = new InMemoryFeedCache();
var jobRepo = new InMemoryJobRepository();
var users = new List<User>
{
new(Guid.NewGuid(), "Alice Chen", "alice@tech.com", "Staff Engineer at Google", "Technology", new() { "Distributed Systems", "Kubernetes", "Go" }),
new(Guid.NewGuid(), "Bob Smith", "bob@startup.io", "CTO at StartupX", "Technology", new() { "Python", "Machine Learning", "AWS" }),
new(Guid.NewGuid(), "Carol Davis", "carol@finance.com", "VP Engineering at FinCorp", "Finance", new() { "Java", "System Design", "Leadership" }),
new(Guid.NewGuid(), "Dave Wilson", "dave@data.co", "Data Scientist at DataCo", "Technology", new() { "Python", "TensorFlow", "SQL" }),
new(Guid.NewGuid(), "Eve Johnson", "eve@design.io", "Product Designer at DesignCo", "Design", new() { "Figma", "UX Research", "Prototyping" }),
};
foreach (var user in users) userRepo.AddUser(user);
var companies = new List<Company>
{
new(Guid.NewGuid(), "TechGiant", "Technology", 150000),
new(Guid.NewGuid(), "StartupX", "Technology", 50),
new(Guid.NewGuid(), "FinCorp", "Finance", 10000),
};
var jobs = new List<JobListing>
{
new(Guid.NewGuid(), companies[0].Id, "Senior Distributed Systems Engineer", "Build large-scale distributed systems", new() { "Distributed Systems", "Go", "Kubernetes" }, "Mountain View, CA", 200000, 350000),
new(Guid.NewGuid(), companies[1].Id, "ML Engineer", "Build recommendation systems", new() { "Machine Learning", "Python", "AWS" }, "San Francisco, CA", 180000, 300000),
new(Guid.NewGuid(), companies[2].Id, "VP of Engineering", "Lead engineering organization", new() { "Leadership", "System Design", "Java" }, "New York, NY", 250000, 400000),
};
foreach (var job in jobs) jobRepo.AddJob(job);
var connectionService = new ConnectionService(connRepo, feedCache);
var feedService = new FeedService(connRepo, postRepo, userRepo, feedCache);
var jobMatchingService = new JobMatchingService(jobRepo, userRepo, connRepo);
var endorsementService = new EndorsementService(connRepo);
var messagingService = new MessagingService();
var notificationService = new NotificationService();
// Create connections
Console.WriteLine("--- Creating Connections ---");
for (int i = 0; i < users.Count; i++)
{
for (int j = i + 1; j < users.Count; j++)
{
var conn = await connectionService.SendRequestAsync(users[i].Id, users[j].Id);
await connectionService.AcceptRequestAsync(conn.Id, users[j].Id);
Console.WriteLine($"Connected: {users[i].Name} <-> {users[j].Name}");
}
}
// Degree of separation
Console.WriteLine("\n--- Degree of Separation ---");
var degree = await connectionService.GetDegreeOfSeparationAsync(users[0].Id, users[3].Id);
Console.WriteLine($"Degree between {users[0].Name} and {users[3].Name}: {degree}");
// Create posts
Console.WriteLine("\n--- Creating Posts ---");
var post1 = new Post { AuthorId = users[0].Id, Content = "Excited to share our new distributed systems architecture!" };
var post2 = new Post { AuthorId = users[1].Id, Content = "Just shipped our ML recommendation engine. Here's what we learned..." };
var post3 = new Post { AuthorId = users[2].Id, Content = "Looking for senior engineers to join our team at FinCorp!" };
await feedService.CreatePostAsync(post1);
await feedService.CreatePostAsync(post2);
await feedService.CreatePostAsync(post3);
Console.WriteLine("3 posts created and fanned out.");
// Get feed
Console.WriteLine("\n--- Feed for " + users[1].Name + " ---");
var feed = await feedService.GetFeedAsync(users[1].Id);
foreach (var item in feed)
{
Console.WriteLine($" [{item.Score:F2}] {item.AuthorName}: {item.Post.Content.Substring(0, Math.Min(50, item.Post.Content.Length))}...");
}
// Job recommendations
Console.WriteLine("\n--- Job Recommendations for " + users[0].Name + " ---");
var jobRecs = await jobMatchingService.GetJobRecommendationsAsync(users[0].Id);
foreach (var sj in jobRecs)
{
Console.WriteLine($" [{sj.Score:F2}] {sj.Job.Title} at {sj.Job.Location} (${sj.Job.MinSalary}-{sj.Job.MaxSalary})");
}
// Endorsements
Console.WriteLine("\n--- Skill Endorsements ---");
await endorsementService.EndorseSkillAsync(users[1].Id, users[0].Id, "Distributed Systems");
await endorsementService.EndorseSkillAsync(users[2].Id, users[0].Id, "Distributed Systems");
await endorsementService.EndorseSkillAsync(users[3].Id, users[0].Id, "Distributed Systems");
var endorsementCount = endorsementService.GetEndorsementCount(users[0].Id, "Distributed Systems");
Console.WriteLine($"{users[0].Name} received {endorsementCount} endorsements for 'Distributed Systems'");
// Messaging
Console.WriteLine("\n--- Messaging ---");
var msg1 = await messagingService.SendMessageAsync(users[0].Id, users[1].Id, "Hey Bob, loved your ML post!");
var msg2 = await messagingService.SendMessageAsync(users[1].Id, users[0].Id, "Thanks Alice! Let's collaborate sometime.");
var msg3 = await messagingService.SendMessageAsync(users[0].Id, users[1].Id, "Absolutely, let's set up a call next week.");
var conversation = await messagingService.GetConversationMessagesAsync(msg1.ConversationId);
Console.WriteLine($"Conversation has {conversation.Count} messages:");
foreach (var msg in conversation)
{
var sender = users.First(u => u.Id == msg.SenderId);
Console.WriteLine($" {sender.Name}: {msg.Body}");
}
// Notifications
Console.WriteLine("\n--- Notifications for " + users[1].Name + " ---");
notificationService.SendNotification(new Notification
{
UserId = users[1].Id,
Type = NotificationType.JobRecommendation,
Message = "3 new jobs match your profile"
});
notificationService.SendNotification(new Notification
{
UserId = users[1].Id,
Type = NotificationType.PostEngagement,
Message = "Your post received 42 reactions"
});
var notifs = notificationService.GetUserNotifications(users[1].Id);
foreach (var n in notifs)
{
Console.WriteLine($" [{n.Type}] {n.Message}");
}
Console.WriteLine("\n=== Demo Complete ===");
}
}
// ============================================================
// ENTRY POINT
// ============================================================
class Program
{
static async Task Main(string[] args)
{
await LinkedInSystemDemo.RunAsync();
}
}
}
This implementation totals 450+ lines of C# and demonstrates the following system design patterns:
- Repository Pattern — Clean separation of data access from business logic
- Dependency Injection — All services receive their dependencies through constructors
- Event-Driven Architecture — Notifications published asynchronously from business operations
- Graph Traversal — BFS-based degree of separation calculation
- Feed Fanout — Write-time fanout for regular users, read-time computation for large accounts
- ML Score Composition — Feature-based scoring for job matching and feed ranking
- Concurrent Data Structures — Thread-safe in-memory stores using ConcurrentDictionary
- Endorsement Weighting — Expertise-weighted skill endorsements
- Messaging with Sequence Numbers — Monotonically increasing sequence for message ordering
- Deterministic IDs — Consistent conversation IDs from participant pairs
25. Conclusion
Designing a professional network at LinkedIn's scale requires mastery of virtually every distributed systems concept. From the graph-based social connection layer to the ML-powered feed ranking, from the real-time messaging infrastructure to the sophisticated job matching engine, each subsystem presents unique challenges in consistency, availability, latency, and scale.
- Co-locate related data — Shard by the most common access pattern (user_id for most LinkedIn data) to minimize cross-shard queries.
- Hybrid consistency models — Use strong consistency for transactions (messaging, job applications) and eventual consistency for reads (feed, counters).
- Fanout strategy must match reality — Fanout-on-write for regular users, fanout-on-read for celebrities. Never force one pattern on all cases.
- ML is not optional at scale — At billions of items, manual curation is impossible. Every content, job, and notification ranking requires ML models.
- Cache aggressively, invalidate smartly — Multi-layer caching with appropriate TTLs and write-through invalidation prevents stale data while maximizing hit rates.
- Event-driven backbone is essential — Kafka as the central nervous system allows hundreds of services to operate independently while maintaining data flow.
For system design interviews, LinkedIn-style questions test breadth across graph algorithms, distributed storage, real-time systems, ML pipelines, and API design. The key to a strong answer is identifying the constraints (read-heavy vs. write-heavy, consistency requirements, scale), choosing appropriate data stores and patterns for each subsystem, and being able to reason about trade-offs at each decision point.
The C# implementation provided in this article demonstrates production-grade patterns that can be adapted to real-world systems. While the in-memory repositories are simplified for demonstration, the service logic, scoring algorithms, and architectural patterns directly translate to distributed implementations backed by databases, caches, and message queues.
Understanding how LinkedIn manages a billion-member professional network provides invaluable insight into building any large-scale social or marketplace system. The patterns learned here — from graph partitioning to feed ranking to notification optimization — apply broadly to any system where millions of users generate and consume interconnected content at massive scale.