system-design48 min read

How to Design Professional Network like LinkedIn — A Senior+ Guide | Ayodhyya

How to Design Professional Network like LinkedIn

Building social graph, job matching, feed, and messaging at 1B+ member scale — A Senior+ Guide by Ayodhyya

Published: July 14, 2026  |  Reading Time: ~45 min  |  System Design Distributed Systems C#

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.

Why Study LinkedIn's System Design?
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

FeatureDescriptionPriority
User Registration & ProfilesCreate accounts, manage professional profiles with work history, education, skills, and mediaP0
Connection ManagementSend, accept, reject connection requests; 1st, 2nd, 3rd degree networkP0
News FeedPersonalized feed with posts from connections, companies, and content creatorsP0
MessagingDirect messaging between connections, InMail for premium, group conversationsP0
Job Listings & ApplicationsPost jobs, search/apply, job alerts, recruiter matchingP0
SearchPeople, jobs, companies, content search with filters and autocompleteP0
NotificationsPush, email, in-app notifications for connections, jobs, messages, mentionsP1
Company PagesCompany profiles, employee count, job postings, follower managementP1
Endorsements & RecommendationsSkill endorsements, written recommendations between professionalsP1
Content PublishingArticles, short-form posts, document carousels, polls, newslettersP1
Recruiter ToolsTalent pipeline, candidate search, outreach tracking, hiring analyticsP1
LinkedIn LearningVideo courses, assessments, certificates, learning pathsP2
Advertising PlatformSponsored content, job ads, message ads, dynamic ads, campaign analyticsP2
Who Viewed Your ProfileProfile view tracking and analytics (premium feature)P2

Non-Functional Requirements

RequirementTargetRationale
Availability99.99% (52 min downtime/year)Professional network used during business hours globally
Latency (p99)< 200ms for reads, < 500ms for writesReal-time feel for feed, messaging, and search
Throughput10M+ requests/second peakBillions of profile views, feed loads, and searches daily
Data Durability99.999999999% (11 nines)Professional data, messages, and recommendations are irreplaceable
ConsistencyEventual consistency for feed; strong for messaging and job applicationsFeed can tolerate staleness; financial/transactional features cannot
ScalabilityLinear horizontal scaling for all servicesGrowth from 1B to 2B+ members must not require re-architecture
SecuritySOC 2, GDPR compliant, end-to-end encryption for messagesProfessional 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.

OperationDaily VolumeQPS (avg)QPS (peak, 3x)
Profile Views8 billion~93,000~280,000
Feed Loads10 billion~116,000~350,000
Connection Requests50 million~580~1,740
Messages Sent10 billion~116,000~350,000
Job Applications100 million~1,160~3,480
Search Queries2 billion~23,000~70,000
Post Creations500 million~5,800~17,400
Notifications Sent30 billion~347,000~1,040,000

Storage Estimates

Data Volume Calculations:

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

erDiagram USER ||--o{ CONNECTION : sends USER ||--o{ CONNECTION : receives USER ||--o{ PROFILE : has USER ||--o{ POST : creates USER ||--o{ MESSAGE : sends USER ||--o{ MESSAGE : receives USER ||--o{ JOB_APPLICATION : submits USER ||--o{ ENDORSEMENT : gives USER ||--o{ ENDORSEMENT : receives USER ||--o{ RECOMMENDATION : writes USER }o--|| COMPANY : works_at COMPANY ||--o{ JOB_LISTING : posts COMPANY ||--o{ COMPANY_PAGE : manages JOB_LISTING ||--o{ JOB_APPLICATION : receives POST ||--o{ REACTION : has POST ||--o{ COMMENT : has USER ||--o{ NOTIFICATION : receives USER }o--o{ GROUP : joins

Core Tables

TablePrimary KeyPartition KeyKey FieldsEstimated Size
usersuser_id (UUID)user_idemail, name, password_hash, status, created_at~500 GB
profilesuser_iduser_idheadline, summary, location, industry, skills[], experience[], education[]~5 TB
connectionsconnection_idsender_idsender_id, receiver_id, status, created_at~25 TB
postspost_idauthor_idauthor_id, content, media_urls, visibility, created_at, engagement_counts~2 TB
messagesmessage_idconversation_idconversation_id, sender_id, body, attachments, read_status, sent_at~100+ TB
conversationsconversation_idconversation_idparticipants[], last_message_at, is_group, title~500 GB
job_listingsjob_idcompany_idcompany_id, title, description, location, salary_range, requirements, status~200 GB
job_applicationsapplication_idjob_idjob_id, applicant_id, resume_url, status, applied_at~1 TB
companiescompany_idcompany_idname, industry, size, logo_url, description, website~50 GB
notificationsnotification_iduser_iduser_id, type, payload, read_status, created_at~5 TB
endorsementsendorsement_idendorsed_user_idendorsed_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

MethodEndpointDescriptionRate Limit
GET/api/v2/users/{userId}/profileFetch user profile1000/min
PUT/api/v2/users/{userId}/profileUpdate profile fields100/min
POST/api/v2/users/{userId}/profile/photoUpload profile photo10/hour
GET/api/v2/users/{userId}/experienceList work experience500/min
POST/api/v2/users/{userId}/experienceAdd work experience50/min

Connection APIs

MethodEndpointDescriptionRate Limit
GET/api/v2/users/{userId}/connectionsList connections (paginated)200/min
POST/api/v2/connections/requestSend connection request50/day (free), 200/day (premium)
PUT/api/v2/connections/{connId}/acceptAccept connection request200/min
DELETE/api/v2/connections/{connId}Remove connection50/min
GET/api/v2/users/{userId}/network/degree/{targetId}Get degree of separation100/min

Feed & Post APIs

MethodEndpointDescription
GET/api/v2/feed?cursor={cursor}&limit=20Get personalized feed
POST/api/v2/postsCreate a new post
GET/api/v2/posts/{postId}Get single post with engagement
POST/api/v2/posts/{postId}/reactionsReact to a post (like, celebrate, etc.)
POST/api/v2/posts/{postId}/commentsComment on a post
GET/api/v2/users/{userId}/feed?cursor={cursor}Get user's post history

Job APIs

MethodEndpointDescription
GET/api/v2/jobs/search?q={query}&location={loc}Search jobs with filters
POST/api/v2/jobsPost a new job listing
GET/api/v2/jobs/{jobId}Get job details
POST/api/v2/jobs/{jobId}/applyApply to a job
GET/api/v2/users/{userId}/job-applicationsList user's applications
GET/api/v2/jobs/recommendationsGet ML-powered job recommendations

Messaging API

MethodEndpointDescription
GET/api/v2/conversations?cursor={cursor}List conversations
POST/api/v2/conversationsCreate new conversation
GET/api/v2/conversations/{convId}/messages?cursor={cursor}Get messages in conversation
POST/api/v2/conversations/{convId}/messagesSend a message
PUT/api/v2/conversations/{convId}/readMark conversation as read
POST/api/v2/inmailSend 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.

graph TB subgraph "Client Layer" WEB[Web App - React SPA] IOS[iOS App] AND[Android App] API_EXT[Public API - OAuth] end subgraph "Edge Layer" CDN[CDN - Akamai/CloudFront] LB[Load Balancer - VIP/L7] GATEWAY[API Gateway - Rate Limit, Auth] end subgraph "Service Mesh" FEED_SVC[Feed Service] PROFILE_SVC[Profile Service] CONN_SVC[Connection Service] MSG_SVC[Messaging Service] JOB_SVC[Job Service] SEARCH_SVC[Search Service] POST_SVC[Content Service] NOTIF_SVC[Notification Service] RECRUIT_SVC[Recruiter Service] ADS_SVC[Ads Service] LEARNING_SVC[Learning Service] ML_SVC[ML/Recommendation Service] GRAPH_SVC[Graph Service] end subgraph "Data Layer" MYSQL[(MySQL Cluster)] CASSANDRA[(Cassandra))] ELASTIC[(Elasticsearch)] GRAPH_DB[(Neo4j/Terminus)] REDIS[(Redis Cluster)] S3[(Object Storage - S3)] KAFKA[(Apache Kafka)] HBASE[(HBase/Bigtable)] end WEB --> CDN IOS --> LB AND --> LB API_EXT --> LB CDN --> LB LB --> GATEWAY GATEWAY --> FEED_SVC GATEWAY --> PROFILE_SVC GATEWAY --> CONN_SVC GATEWAY --> MSG_SVC GATEWAY --> JOB_SVC GATEWAY --> SEARCH_SVC GATEWAY --> POST_SVC GATEWAY --> NOTIF_SVC GATEWAY --> RECRUIT_SVC GATEWAY --> ADS_SVC GATEWAY --> LEARNING_SVC FEED_SVC --> KAFKA POST_SVC --> KAFKA CONN_SVC --> KAFKA MSG_SVC --> KAFKA NOTIF_SVC --> KAFKA JOB_SVC --> KAFKA ML_SVC --> KAFKA FEED_SVC --> REDIS FEED_SVC --> MYSQL PROFILE_SVC --> MYSQL PROFILE_SVC --> S3 CONN_SVC --> GRAPH_DB CONN_SVC --> REDIS MSG_SVC --> CASSANDRA MSG_SVC --> HBASE SEARCH_SVC --> ELASTIC JOB_SVC --> MYSQL JOB_SVC --> ELASTIC POST_SVC --> CASSANDRA POST_SVC --> S3 NOTIF_SVC --> CASSANDRA GRAPH_SVC --> GRAPH_DB LEARNING_SVC --> MYSQL LEARNING_SVC --> S3 ADS_SVC --> MYSQL ML_SVC --> MYSQL ML_SVC --> REDIS
Architecture Principle: Every user-facing operation is handled by a dedicated domain service. Services communicate synchronously via gRPC for latency-sensitive paths (feed, profile) and asynchronously via Kafka for write-heavy, fan-out-heavy paths (notifications, feed fanout, search indexing). This separation allows independent scaling and deployment of each subsystem.

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.

graph LR subgraph "Graph Partition 1 - Users 0-250M" A1[User A] --> B1[User B] A1 --> C1[User C] A1 --> D1[User D] B1 --> E1[User E] end subgraph "Graph Partition 2 - Users 250M-500M" F1[User F] --> G1[User G] F1 --> H1[User H] G1 --> I1[User I] end subgraph "Graph Partition 3 - Users 500M-750M" J1[User J] --> K1[User K] J1 --> L1[User L] K1 --> M1[User M] end subgraph "Graph Partition 4 - Users 750M-1B" N1[User N] --> O1[User O] N1 --> P1[User P] O1 --> Q1[User Q] end A1 -.->|cross-partition edge| G1 F1 -.->|cross-partition edge| K1 J1 -.->|cross-partition edge| O1

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

sequenceDiagram participant UserA as User A (Sender) participant Gateway as API Gateway participant ConnSvc as Connection Service participant GraphDB as Graph Database participant Kafka as Kafka participant NotifSvc as Notification Service participant UserB as User B (Receiver) UserA->>Gateway: POST /connections/request {userId: B} Gateway->>ConnSvc: CreateConnectionRequest(A, B) ConnSvc->>GraphDB: Check existing connection GraphDB-->>ConnSvc: No existing edge ConnSvc->>GraphDB: Create edge (status: PENDING) ConnSvc->>Kafka: Publish ConnectionRequested event Kafka->>NotifSvc: Consume event NotifSvc->>UserB: Push notification + email ConnSvc-->>Gateway: 201 Created Gateway-->>UserA: Connection request sent UserB->>Gateway: PUT /connections/{connId}/accept Gateway->>ConnSvc: AcceptConnection(connId) ConnSvc->>GraphDB: Update edge (status: ACCEPTED) ConnSvc->>Kafka: Publish ConnectionAccepted event Kafka->>NotifSvc: Consume event NotifSvc->>UserA: "B accepted your connection" ConnSvc-->>Gateway: 200 OK Gateway-->>UserB: Connection established

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.

graph TB subgraph "Post Creation" AUTHOR[Author Creates Post] VALIDATE[Validate & Store Post] FANOUT{Follower Count < 10K?} end subgraph "Fanout-on-Write Path" FEED_WRITER[Feed Writer Service] PRECOMP_FEED[(Pre-computed Feed Cache)] USER_FEED_A[User A Feed] USER_FEED_B[User B Feed] USER_FEED_N[User N Feed] end subgraph "Fanout-on-Read Path" FEED_READER[Feed Reader Service] CELEBRITY_POSTS[Celebrity Post Pool] ML_RANKER[ML Ranking Model] MERGE[Merge & Sort] end subgraph "Feed Retrieval" CLIENT[Client Request] FEED_SVC[Feed Service] REDIS_FEED[(Redis Feed Cache)] FINAL_FEED[Final Ranked Feed] end AUTHOR --> VALIDATE VALIDATE --> FANOUT FANOUT -->|Yes| FEED_WRITER FANOUT -->|No| CELEBRITY_POSTS FEED_WRITER --> PRECOMP_FEED PRECOMP_FEED --> USER_FEED_A PRECOMP_FEED --> USER_FEED_B PRECOMP_FEED --> USER_FEED_N CLIENT --> FEED_SVC FEED_SVC --> REDIS_FEED REDIS_FEED --> FEED_READER CELEBRITY_POSTS --> FEED_READER FEED_READER --> ML_RANKER ML_RANKER --> MERGE MERGE --> FINAL_FEED

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

graph TB subgraph "Job Posting" RECRUITER[Recruiter Posts Job] PARSE[Parse Job Description] EXTRACT[Extract Requirements] EMBED[Generate Embedding Vector] INDEX[Index in Search & Vector DB] end subgraph "Candidate Matching" CANDIDATE[Job Seeker Profile] PREFER[Extract Preferences] CAND_EMBED[Generate Candidate Embedding] RETRIEVE[Retrieve Matching Candidates] RANK_ML[Rank by Match Score] end subgraph "Job Recommendations" USER_SIGNALS[User Interaction Signals] COLLAB[F Collaborative Filtering] CONTENT_BASED[Content-Based Filtering] ENSEMBLE[Ensemble Ranker] PERSONALIZE[Personalized Job Feed] end RECRUITER --> PARSE PARSE --> EXTRACT EXTRACT --> EMBED EMBED --> INDEX CANDIDATE --> PREFER PREFER --> CAND_EMBED CAND_EMBED --> RETRIEVE RETRIEVE --> RANK_ML USER_SIGNALS --> COLLAB USER_SIGNALS --> CONTENT_BASED COLLAB --> ENSEMBLE CONTENT_BASED --> ENSEMBLE ENSEMBLE --> PERSONALIZE INDEX --> RETRIEVE CAND_EMBED --> COLLAB

Matching Score Components

FeatureWeightDescription
Skills Match0.30Overlap between candidate skills and job requirements (NLP extracted)
Experience Level0.20Years of experience and seniority alignment
Education Match0.10Degree level and field relevance
Location Preference0.15Geographic compatibility, remote/on-site preference
Salary Alignment0.10Expected vs. offered compensation range overlap
Industry Fit0.05Industry background relevance
Company Interest0.05Historical interaction with the company (views, follows)
Apply Probability0.05ML-predicted probability of application given impression
Key Insight: LinkedIn's job matching achieves a match rate of over 40%, meaning that 4 out of 10 recommended jobs result in meaningful candidate engagement (view, save, or apply). This is powered by a continuous learning pipeline that retrains models weekly on new interaction data, with A/B testing infrastructure evaluating dozens of ranking variants simultaneously.

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

graph TB subgraph "Message Producer" SENDER[Sender Client] MSG_API[Messaging API] CONV_SVC[Conversation Service] end subgraph "Message Processing" KAFKA_MSG[(Kafka - Messages Topic)] MSG_PROCESSOR[Message Processor] PRESENCE_SVC[Presence Service] NOTIF_DISPATCH[Notification Dispatcher] end subgraph "Message Storage" CASSANDRA_MSG[(Cassandra - Message Store)] REDIS_CONV[(Redis - Conversation Cache)] SEARCH_IDX[(Elasticsearch - Message Search)] end subgraph "Message Consumer" WS_SERVER[WebSocket Server] RECEIVER[Receiver Client] PUSH_SVC[Push Notification Service] EMAIL_SVC[Email Service] end SENDER --> MSG_API MSG_API --> CONV_SVC CONV_SVC --> KAFKA_MSG KAFKA_MSG --> MSG_PROCESSOR KAFKA_MSG --> SEARCH_IDX MSG_PROCESSOR --> CASSANDRA_MSG MSG_PROCESSOR --> REDIS_CONV MSG_PROCESSOR --> PRESENCE_SVC MSG_PROCESSOR --> NOTIF_DISPATCH PRESENCE_SVC --> WS_SERVER WS_SERVER --> RECEIVER NOTIF_DISPATCH --> PUSH_SVC NOTIF_DISPATCH --> EMAIL_SVC PUSH_SVC --> RECEIVER

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

EntityKey FieldsRelationships
company_pagescompany_id, name, industry, size, logo, description, website, headquartersHas many administrators, followers, jobs, posts
company_adminsuser_id, company_id, role (admin/super_admin/content_admin), permissionsMany-to-one with companies and users
company_followersuser_id, company_id, followed_at, notification_preferenceMany-to-many between users and companies
company_insightscompany_id, date, follower_count, page_views, unique_visitors, job_clicksTime-series data partitioned by company_id and month
company_updatesupdate_id, company_id, content, media, posted_by, created_atCompany 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

graph LR subgraph "Upload" CREATOR[Content Creator] UPLOAD[Upload Service] CDN_UPLOAD[(CDN)] end subgraph "Processing" QUEUE[Kafka Upload Queue] THUMBNAIL[Thumbnail Generator] VIDEO_PROC[Video Transcoding] MODERATION[Content Moderation - AI] SPAM[Spam Detection] end subgraph "Storage" BLOB[(Object Storage)] DB[(Content Database)] INDEX[(Search Index)] end subgraph "Distribution" FANOUT[Fanout Service] FEED_SVC[Feed Distribution] NOTIF[Notification Trigger] end CREATOR --> UPLOAD UPLOAD --> CDN_UPLOAD CDN_UPLOAD --> QUEUE QUEUE --> THUMBNAIL QUEUE --> VIDEO_PROC QUEUE --> MODERATION QUEUE --> SPAM THUMBNAIL --> BLOB VIDEO_PROC --> BLOB MODERATION --> DB DB --> INDEX DB --> FANOUT FANOUT --> FEED_SVC FANOUT --> NOTIF

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.

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

graph TB subgraph "Query Processing" QUERY[Search Query] PARSE[Query Parser] EXPAND[Query Expansion - Synonyms] PERSONALIZE[Personalization Layer] end subgraph "Retrieval" ES_PRIMARY[(Elasticsearch Primary)] ES_REPLICA[(Elasticsearch Replica)] VEC_DB[(Vector Database - ANN)] SPELL_CHECK[Spell Correction] end subgraph "Ranking" L1[L1 - BM25 Text Match] L2[L2 - Learning to Rank] L3[L3 - Personalized Re-ranking] AD_INJECT[Ad Injection] end subgraph "Response" FORMAT[Response Formatter] SPELL_SUGGEST[Did You Mean?] FACET計算[Facets Computation] end QUERY --> PARSE PARSE --> EXPAND EXPAND --> PERSONALIZE PERSONALIZE --> ES_PRIMARY PERSONALIZE --> VEC_DB ES_PRIMARY --> L1 ES_REPLICA --> L1 VEC_DB --> L1 L1 --> L2 L2 --> L3 L3 --> AD_INJECT AD_INJECT --> FORMAT FORMAT --> SPELL_SUGGEST FORMAT --> FACET計算

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

FeatureTypeImpact
Keyword Match ScoreTextBM25 relevance of query terms against profile fields
Profile CompletenessProfileComplete profiles ranked higher (premium signal)
Connection DegreeGraph1st-degree connections boosted in results
Mutual ConnectionsGraphMore mutual connections = higher rank
Engagement RecencyBehavioralRecently active profiles ranked higher
Query-Click FeedbackBehavioralProfiles frequently clicked for similar queries boosted
Premium StatusMonetizationPremium subscribers get slight ranking boost
Hiring IntentBehavioralUsers 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.

Notification Fatigue Problem: With 30B+ potential notifications daily, sending all of them would overwhelm users. LinkedIn uses a notification scoring model that predicts the probability of user engagement (open, click, dismiss) for each notification. Notifications below a threshold are suppressed, batched into digests, or deprioritized. This model reduces notification volume by approximately 60% while maintaining 95% of engagement.

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

ComponentTechnologyScale
Video StorageObject storage (S3) + CDN500TB+ of video content
Video StreamingHLS/DASH with adaptive bitrate10M+ concurrent streams at peak
Course MetadataMySQL + Elasticsearch21K courses, 200K+ videos
Progress TrackingCassandra (time-series)Billions of progress events
AssessmentsQuestion bank + proctoring service50+ skill assessments
RecommendationsML 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

DimensionExamplesGranularity
Job TitleSoftware Engineer, VP of Marketing, CEOIndividual titles + title categories
CompanySpecific companies, company size, industryExact match + category
SkillsPython, Machine Learning, Project ManagementIndividual skills + skill clusters
EducationUniversity, degree, field of studyExact + category
LocationCountry, state, city, metro areaGeographic hierarchy
IndustryTechnology, Healthcare, Finance2-digit and 4-digit NAICS codes
Experience LevelEntry, Senior, Director, VP, C-SuiteSeniority levels
InterestsLinkedIn Learning topics, groups joinedTopic 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 TypeShard KeyStrategyReplicas
Users/Profilesuser_id (hash)Consistent hashing across 1024 shards3 per shard
Connectionsuser_id (hash)Co-located with user profile shard3 per shard
Messagesconversation_id (hash)Co-locate conversation messages3 per shard
Postsauthor_id (hash)Co-located with author profile3 per shard
Job Listingscompany_id (hash)Co-locate with company page3 per shard
Notificationsuser_id (hash)Co-located with user profile3 per shard
Feed Cacheuser_id (hash)Redis cluster with consistent hashing2 per shard
Sharding Principle: LinkedIn co-locates related data on the same shard whenever possible. User profiles, their connections, their posts, their notifications, and their feed cache all share the same shard key (user_id). This minimizes cross-shard queries, which are the primary source of latency in distributed databases. The trade-off is potential hotspot creation for power users (celebrities with millions of connections), which is handled with separate read replicas and fanout-on-read strategies.

20. Caching Strategy

LinkedIn employs a multi-layered caching architecture with different TTLs and eviction strategies for each data type.

Cache Hierarchy

LayerTechnologyData TypesTTLHit Rate Target
L1 - BrowserService Worker + IndexedDBProfile fragments, feed items5-30 min40%
L2 - CDNAkamai/CloudFrontStatic assets, profile photos, media24 hours85%
L3 - ApplicationRedis Cluster (in-memory)Feed cache, session data, counters1-60 min92%
L4 - DatabaseMySQL buffer pool + query cacheHot profile data, recent messagesN/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.

graph TB subgraph "US-EAST (Primary)" LB_US[Load Balancer] SVC_US[Application Services] DB_US[(MySQL Primary)] REDIS_US[(Redis Cluster)] KAFKA_US[(Kafka Cluster)] ES_US[(Elasticsearch)] end subgraph "EU-WEST" LB_EU[Load Balancer] SVC_EU[Application Services] DB_EU[(MySQL Primary)] REDIS_EU[(Redis Cluster)] KAFKA_EU[(Kafka Cluster)] ES_EU[(Elasticsearch)] end subgraph "APAC-SOUTH" LB_AP[Load Balancer] SVC_AP[Application Services] DB_AP[(MySQL Primary)] REDIS_AP[(Redis Cluster)] KAFKA_AP[(Kafka Cluster)] ES_AP[(Elasticsearch)] end subgraph "Global Services" DNS[Global DNS - Route53] CROSS_REGION[Cross-Region Replication] ML_GLOBAL[ML Training - US Primary] end DNS --> LB_US DNS --> LB_EU DNS --> LB_AP DB_US -.->|async replication| DB_EU DB_US -.->|async replication| DB_AP KAFKA_US -.->|mirror maker| KAFKA_EU KAFKA_US -.->|mirror maker| KAFKA_AP CROSS_REGION --> DB_US CROSS_REGION --> DB_EU CROSS_REGION --> DB_AP ML_GLOBAL --> SVC_US

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

ComponentConfigurationMonthly Cost (Est.)
Application Servers5000 instances (m5.2xlarge equivalent)$1,500,000
MySQL Cluster100 shards × 3 replicas (r5.4xlarge)$1,200,000
Cassandra Cluster2000 nodes (i3.2xlarge)$1,000,000
Redis Cluster500 nodes (r5.xlarge)$300,000
Elasticsearch300 data nodes (r5.2xlarge)$500,000
Kafka Cluster200 brokers (kafka.m5.2xlarge)$250,000
Object Storage (S3)5 PB stored + transfers$200,000
CDN100 PB/month transfer$800,000
ML/GPU Infrastructure200 GPU instances (p3.2xlarge)$600,000
Networking & Data TransferCross-region + internet egress$400,000
Monitoring & ObservabilityDatadog/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
Cost Per User: At $7M/month infrastructure cost and 350M monthly active users, the infrastructure cost per MAU is approximately $0.02/month or $0.24/year. LinkedIn's revenue per member is approximately $10-15/year, giving a healthy margin for infrastructure investment. The largest cost drivers are compute (application servers) and managed database services.

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.

Key Architectural Takeaways:
  • 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.

© 2026 Ayodhyya. All rights reserved.

System Design Series | Built for senior+ engineers preparing for architecture interviews.