System Design
  • Introduction
  • Glossary of System Design
    • System Design Basics
    • Key Characteristics of Distributed Systems
    • Scalability - Harvard lecture
      • Scalability for Dummies - Part 1: Clones
      • Scalability for Dummies - Part 2: Database
      • Scalability for Dummies - Part 3: Cache
      • Scalability for Dummies - Part 4: Asynchronism
    • Trade-off
      • CAP Theorem
      • Performance vs scalability
      • Latency vs throughput
      • Availability vs consistency
    • Load Balancing
      • Load balancer
    • Proxies
      • Reverse proxy
    • Cache
      • Caching
    • Asynchronism
    • Processing guarantee in Kafka
    • Database
      • Relational database management system (RDBMS)
      • Redundancy and Replication
      • Data Partitioning
      • Indexes
      • NoSQL
      • SQL vs. NoSQL
      • Consistent Hashing
    • Application layer
    • DNS
    • CDN
    • Communication
      • Long-Polling vs WebSockets vs Server-Sent Events
    • Security
    • Lambda Architecture
  • OOD design
    • Concepts
      • Object-Oriented Basics
      • OO Analysis and Design
      • What is UML?
      • Use Case Diagrams
    • Design a parking lot
  • System Design Cases
    • Overview
    • Design a system that scales to millions of users on AWS
    • Designing a URL Shortening service like TinyURL
      • Design Unique ID Generator
      • Designing Pastebin
      • Design Pastebin.com (or Bit.ly)
    • Design notification system (scott)
      • Designing notification service
    • Designing Chat System
      • Designing Slack
      • Designing Facebook Messenger
    • Design Top K System
    • Designing Instagram
    • Design a newsfeed system
      • Designing Facebook’s Newsfeed
      • Design the data structures for a social network
    • Designing Twitter
      • Design the Twitter timeline and search
      • Designing Twitter Search
    • Design Youtube - Scott
      • Design live commenting
      • Designing Youtube or Netflix
    • Designing a Web Crawler
      • Designing a distributed job scheduler
      • Designing a Web Crawler/Archive (scott)
      • Design a web crawler
    • Designing Dropbox
    • Design Google Doc
    • Design Metrics Aggregation System
      • Design Ads Logging System
    • Design Instacart
    • Design a payment system
      • Airbnb - Avoiding Double Payments in a Distributed Payments System
    • Design Distributed Message Queue
      • Cherami: Uber Engineering’s Durable and Scalable Task Queue in Go
    • Design Distributed Cache
      • Design a key-value cache to save the results of the most recent web server queries
    • Design a scalable file distribution system
    • Design Amazon's sales ranking by category feature
    • Design Mint.com
    • Design Autocomplete System
      • Designing Typeahead Suggestion
    • Designing an API Rate Limiter
      • Designing Rate Limiter
    • Design Google Map
      • Designing Yelp or Nearby Friends
      • Designing Uber backend
    • Designing Ticketmaster
      • Design 12306 - Scott
    • Design AirBnB or a Hotel Booking System
  • Paper Reading
    • MapReduce
  • Other Questions
    • What happened after you input the url in the browser?
Powered by GitBook
On this page
  • Design the data structures for a social network
  • Step 1: Outline use cases and constraints
  • Use cases
  • Constraints and assumptions
  • Step 2: Create a high level design
  • Step 3: Design core components
  • Use case: User searches for someone and sees the shortest path to the searched person
  • Step 4: Scale the design
  • Additional talking points

Was this helpful?

  1. System Design Cases
  2. Design a newsfeed system

Design the data structures for a social network

Design the data structures for a social network

Step 1: Outline use cases and constraints

Use cases

  • User searches for someone and sees the shortest path to the searched person

  • Service has high availability

Constraints and assumptions

State assumptions

  • Traffic is not evenly distributed

    • Some searches are more popular than others, while others are only executed once

  • Graph data won't fit on a single machine

  • Graph edges are unweighted

  • 100 million users

  • 50 friends per user average

  • 1 billion friend searches per month

Calculate usage

  • 5 billion friend relationships

    • 100 million users * 50 friends per user average

  • 400 search requests per second

Handy conversion guide:

  • 2.5 million seconds per month

  • 1 request per second = 2.5 million requests per month

  • 40 requests per second = 100 million requests * per month

  • 400 requests per second = 1 billion requests per month

Step 2: Create a high level design

Step 3: Design core components

Use case: User searches for someone and sees the shortest path to the searched person

Without the constraint of millions of users (vertices) and billions of friend relationships (edges), we could solve this unweighted shortest path task with a general BFS approach:

class Graph(Graph):

    def shortest_path(self, source, dest):
        if source is None or dest is None:
            return None
        if source is dest:
            return [source.key]
        prev_node_keys = self._shortest_path(source, dest)
        if prev_node_keys is None:
            return None
        else:
            path_ids = [dest.key]
            prev_node_key = prev_node_keys[dest.key]
            while prev_node_key is not None:
                path_ids.append(prev_node_key)
                prev_node_key = prev_node_keys[prev_node_key]
            return path_ids[::-1]

    def _shortest_path(self, source, dest):
        queue = deque()
        queue.append(source)
        prev_node_keys = {source.key: None}
        source.visit_state = State.visited
        while queue:
            node = queue.popleft()
            if node is dest:
                return prev_node_keys
            prev_node = node
            for adj_node in node.adj_nodes.values():
                if adj_node.visit_state == State.unvisited:
                    queue.append(adj_node)
                    prev_node_keys[adj_node.key] = prev_node.key
                    adj_node.visit_state = State.visited
        return None
  • The Client sends a request to the Web Server, running as a reverse proxy

  • The Web Server forwards the request to the Search API server

  • The Search API server forwards the request to the User Graph Service

  • The User Graph Service does the following:

    • Uses the Lookup Service to find the Person Server where the current user's info is stored

    • Finds the appropriate Person Server to retrieve the current user's list of friend_ids

    • Runs a BFS search using the current user as the source and the current user's friend_ids as the ids for each adjacent_node

    • To get the adjacent_node from a given id:

      • The User Graph Service will again need to communicate with the Lookup Service to determine which Person Server stores the adjacent_node matching the given id (potential for optimization)

Note: Error handling is excluded below for simplicity. Ask if you should code proper error handing.

Lookup Service implementation:

class LookupService(object):

    def __init__(self):
        self.lookup = self._init_lookup()  # key: person_id, value: person_server

    def _init_lookup(self):
        ...

    def lookup_person_server(self, person_id):
        return self.lookup[person_id]

Person Server implementation:

class PersonServer(object):

    def __init__(self):
        self.people = {}  # key: person_id, value: person

    def add_person(self, person):
        ...

    def people(self, ids):
        results = []
        for id in ids:
            if id in self.people:
                results.append(self.people[id])
        return results

Person implementation:

class Person(object):

    def __init__(self, id, name, friend_ids):
        self.id = id
        self.name = name
        self.friend_ids = friend_ids

User Graph Service implementation:

class UserGraphService(object):

    def __init__(self, lookup_service):
        self.lookup_service = lookup_service

    def person(self, person_id):
        person_server = self.lookup_service.lookup_person_server(person_id)
        return person_server.people([person_id])

    def shortest_path(self, source_key, dest_key):
        if source_key is None or dest_key is None:
            return None
        if source_key is dest_key:
            return [source_key]
        prev_node_keys = self._shortest_path(source_key, dest_key)
        if prev_node_keys is None:
            return None
        else:
            # Iterate through the path_ids backwards, starting at dest_key
            path_ids = [dest_key]
            prev_node_key = prev_node_keys[dest_key]
            while prev_node_key is not None:
                path_ids.append(prev_node_key)
                prev_node_key = prev_node_keys[prev_node_key]
            # Reverse the list since we iterated backwards
            return path_ids[::-1]

    def _shortest_path(self, source_key, dest_key, path):
        # Use the id to get the Person
        source = self.person(source_key)
        # Update our bfs queue
        queue = deque()
        queue.append(source)
        # prev_node_keys keeps track of each hop from
        # the source_key to the dest_key
        prev_node_keys = {source_key: None}
        # We'll use visited_ids to keep track of which nodes we've
        # visited, which can be different from a typical bfs where
        # this can be stored in the node itself
        visited_ids = set()
        visited_ids.add(source.id)
        while queue:
            node = queue.popleft()
            if node.key is dest_key:
                return prev_node_keys
            prev_node = node
            for friend_id in node.friend_ids:
                if friend_id not in visited_ids:
                    friend_node = self.person(friend_id)
                    queue.append(friend_node)
                    prev_node_keys[friend_id] = prev_node.key
                    visited_ids.add(friend_id)
        return None

We'll use a public REST API:

$ curl https://social.com/api/v1/friend_search?person_id=1234

Response:

{
    "person_id": "100",
    "name": "foo",
    "link": "https://social.com/foo",
},
{
    "person_id": "53",
    "name": "bar",
    "link": "https://social.com/bar",
},
{
    "person_id": "1234",
    "name": "baz",
    "link": "https://social.com/baz",
},

Step 4: Scale the design

Important: Do not simply jump right into the final design from the initial design!

It's important to discuss what bottlenecks you might encounter with the initial design and how you might address each of them. For example, what issues are addressed by adding a Load Balancer with multiple Web Servers? CDN? Master-Slave Replicas? What are the alternatives and Trade-Offs for each?

We'll introduce some components to complete the design and to address scalability issues. Internal load balancers are not shown to reduce clutter.

Below are further optimizations:

  • Store complete or partial BFS traversals to speed up subsequent lookups in the Memory Cache

  • Batch compute offline then store complete or partial BFS traversals to speed up subsequent lookups in a NoSQL Database

  • Reduce machine jumps by batching together friend lookups hosted on the same Person Server

  • Do two BFS searches at the same time, one starting from the source, and one from the destination, then merge the two paths

  • Set a limit based on time or number of hops before asking the user if they want to continue searching, as searching could take a considerable amount of time in some cases

Additional talking points

Additional topics to dive into, depending on the problem scope and time remaining.

SQL scaling patterns

NoSQL

Caching

  • Where to cache

  • What to cache

  • When to update the cache

Asynchronism and microservices

Communications

  • Discuss tradeoffs:

Security

Latency numbers

Ongoing

  • Continue benchmarking and monitoring your system to address bottlenecks as they come up

  • Scaling is an iterative process

PreviousDesigning Facebook’s NewsfeedNextDesigning Twitter

Last updated 4 years ago

Was this helpful?

Exercise the use of more traditional systems - don't use graph-specific solutions such as or a graph database like

We won't be able to fit all users on the same machine, we'll need to users across Person Servers and access them with a Lookup Service.

For internal communications, we could use .

State you would 1) Benchmark/Load Test, 2) Profile for bottlenecks 3) address bottlenecks while evaluating alternatives and trade-offs, and 4) repeat. See as a sample on how to iteratively scale the initial design.

To avoid repeating discussions, refer to the following for main talking points, tradeoffs, and alternatives:

To address the constraint of 400 average read requests per second (higher at peak), person data can be served from a Memory Cache such as Redis or Memcached to reduce response times and to reduce traffic to downstream services. This could be especially useful for people who do multiple searches in succession and for people who are well-connected. Reading 1 MB sequentially from memory takes about 250 microseconds, while reading from SSD takes 4x and from disk takes 80x longer.

Person Servers by location to further improve this, as friends generally live closer to each other

Start the BFS search from people with large numbers of friends, as they are more likely to reduce the number of between the current user and the search target

Use a Graph Database such as or a graph-specific query language such as (if there were no constraint preventing the use of Graph Databases)

External communication with clients -

Internal communications -

Refer to the .

See .

GraphQL
Neo4j
shard
Remote Procedure Calls
Design a system that scales to millions of users on AWS
system design topics
DNS
Load balancer
Horizontal scaling
Web server (reverse proxy)
API server (application layer)
Cache
Consistency patterns
Availability patterns
1
Shard
degrees of separation
Neo4j
GraphQL
Read replicas
Federation
Sharding
Denormalization
SQL Tuning
Key-value store
Document store
Wide column store
Graph database
SQL vs NoSQL
Client caching
CDN caching
Web server caching
Database caching
Application caching
Caching at the database query level
Caching at the object level
Cache-aside
Write-through
Write-behind (write-back)
Refresh ahead
Message queues
Task queues
Back pressure
Microservices
HTTP APIs following REST
RPC
Service discovery
security section
Latency numbers every programmer should know