When you’re asked to design a social‑graph service in a system‑design interview, the interviewer wants to see how you translate vague business goals into concrete components, how you reason about scale, and how you handle the inevitable trade‑offs. Below is a practical walkthrough you can follow in real time, with concrete examples you can adapt on the fly.
1. Clarify the problem scope
Start by confirming the core use‑case. A typical prompt sounds like:
"Design a service that stores and serves friendship relationships for a large social network. It should support adding/removing friends and retrieving a user’s friend list quickly."
Ask follow‑up questions to bound the problem:
- What operations are most important? (e.g.,
AddFriend,RemoveFriend,GetFriends,SuggestFriends) - What latency targets do we have? (e.g., sub‑100 ms for reads, sub‑200 ms for writes)
- How large is the user base? Use a variable like
Ufor total users andFfor average friends per user. - Are we dealing with directed or undirected edges? (friendship is usually bidirectional, follow is directed.)
- Do we need real‑time updates for feeds? This determines whether we push updates or compute them lazily.
- What consistency guarantees are required? Strong consistency for writes, eventual consistency for reads is a common compromise.
By the end of this step you should have a concise problem statement, e.g., "Store bidirectional friendships for up to U ≈ 500 M users, each having up to F ≈ 5 K friends, with read latency < 100 ms and write latency < 200 ms."
2. Functional requirements
| Requirement | Description |
|---|---|
AddFriend(userA, userB) | Create a bidirectional edge; reject if already friends or blocked. |
RemoveFriend(userA, userB) | Delete the edge; must be idempotent. |
GetFriends(userId, limit, offset) | Return a paginated list of friends, sorted by most recent interaction. |
SuggestFriends(userId, K) | Return up to K candidates based on mutual friends or similarity scores. |
BlockUser(userId, targetId) | Prevent future edges and hide existing ones. |
Non‑functional goals include:
- Scalability – handle growth in
UandFwithout redesign. - Low latency – reads dominate, so caching is essential.
- High availability – tolerate node failures; graceful degradation is acceptable.
- Data durability – friendships are critical user data; must survive crashes.
- Observability – metrics for request latency, error rates, and hot‑spot detection.
3. Core data model
A social graph can be represented as an adjacency list stored in a key‑value store. For each user uid, keep a set of friend IDs:
{
"uid": 12345,
"friends": [67890, 11223, …]
}
Because friendships are symmetric, you write to both uid and friendId rows on AddFriend. This duplication simplifies reads but introduces write amplification – a point you’ll discuss later.
Choosing the storage layer
- Primary store – a distributed NoSQL database (e.g., Cassandra, DynamoDB) that offers fast writes, tunable consistency, and horizontal scaling.
- Cache – an in‑memory layer (Redis or Memcached) keyed by
uidfor the most‑active users. TTL can be short because writes invalidate the cache. - Search index – for
SuggestFriends, a graph‑processing engine (e.g., Neo4j, JanusGraph) or a pre‑computed similarity table stored in a columnar store.
4. High‑level architecture
Below is a text diagram you can sketch on a whiteboard:
+-------------------+ +-------------------+ +-------------------+
| API Gateway | ---> | Service Layer | ---> | NoSQL Store |
+-------------------+ +-------------------+ +-------------------+
| | |
| | |
v v v
+-----------+ +-----------+ +-----------+
| Cache | <-------> | Worker | <-------> | Graph |
+-----------+ +-----------+ +-----------+
^ ^ ^
| | |
+-----------+ +-----------+ +-----------+
| Metrics | | Queue | | Backup |
+-----------+ +-----------+ +-----------+
- API Gateway handles authentication, rate‑limiting, and routing.
- Service Layer implements the business logic for the API calls.
- Cache stores hot friend lists; on a miss, the service fetches from the primary store and populates the cache.
- Worker processes asynchronous tasks such as updating suggestion indexes or propagating blocks.
- Queue (Kafka, SQS) decouples write‑heavy paths from the read‑heavy path.
- Metrics and Backup components provide observability and durability.
5. Deep dive: Write path and fan‑out
When a user adds a friend, you must write two rows (one for each direction). If F is large, this can become a bottleneck. Two common patterns mitigate the issue:
- Batch writes – accumulate multiple friendship updates in a time window (e.g., 10 ms) and write them in a single batch operation. This reduces per‑request overhead.
- Fan‑out service – offload the second write to an asynchronous worker. The API returns success after the first write; the worker completes the reciprocal edge later. This trades strict consistency for higher throughput, which is acceptable if the UI can tolerate a brief inconsistency.
Explain the trade‑off: immediate consistency vs. latency. If the interviewer pushes for strong consistency, you can argue for a two‑phase commit limited to a single partition (sharding by uid), noting the added latency.
6. Deep dive: Read path and pagination
GetFriends is read‑heavy. Use the cache for the most active users. For pagination, store friends in a sorted set (by interaction timestamp) so you can fetch a range efficiently. When a user scrolls, the client sends offset and limit; the service translates this to a range query on the sorted set.
If the friend list exceeds cache capacity, fall back to the NoSQL store with a range scan. Explain how you would detect hot keys (e.g., users with > 10 K friends) and shard them across multiple nodes to avoid hotspotting.
7. Suggestion engine
Generating friend suggestions is the hardest part. Two practical approaches:
- Pre‑compute mutual‑friend counts nightly and store top candidates per user. This yields fast reads at the cost of stale data.
- On‑demand graph traversal using a lightweight graph engine that explores 2‑hop neighborhoods. This is slower but always fresh.
In most interviews, you can outline the pre‑compute pipeline: a batch job reads the adjacency lists, computes mutual‑friend scores, and writes the top‑K candidates to a dedicated table. Mention that you would use a map‑reduce style job or a Spark job for scalability.
8. Trade‑offs and scaling decisions
| Aspect | Option A (Simple) | Option B (Scalable) |
|---|---|---|
| Storage | Single NoSQL table | Separate tables for friends, blocks, suggestions |
| Write consistency | Synchronous two‑write | Asynchronous fan‑out worker |
| Read latency | Direct read from cache | Cache + fallback to store |
| Complexity | Low | Higher (additional services) |
| Failure mode | Partial write leads to inconsistency | Worker retries ensure eventual consistency |
Discuss why you might start with Option A for a prototype and migrate to Option B as traffic grows. Emphasize that the interview is about reasoning, not about picking the “perfect” solution.
9. Common follow‑up questions
| Question | How to answer |
|---|---|
| How would you handle a user with millions of friends? | Explain sharding by user ID, using a range‑partitioned store, and possibly a secondary index for the most active friends. |
| What if we need to support directed follows in addition to friendships? | Add a relationship_type field; store follows in a separate adjacency list to keep bidirectional logic simple. |
| How do you protect against hot‑spot attacks (e.g., a celebrity being queried millions of times per second)? | Use request throttling at the API gateway, cache aggressively, and spread the celebrity’s adjacency list across multiple shards. |
| Can you make the suggestion engine real‑time? | Describe a push‑based approach where each new friendship triggers an update to the suggestion list of the two users’ 2‑hop neighbors via a streaming pipeline. |
| What metrics would you monitor? | Latency percentiles for reads/writes, cache hit ratio, queue backlog size, error rates, and node CPU/memory usage. |
10. Sample answer snippet (45‑90 seconds)
"The core of the service is an adjacency list stored in a distributed key‑value store. For each user we keep a set of friend IDs. Adding a friend writes two rows – one for each direction – and we invalidate the cache for both users. Reads are served from an in‑memory cache for hot users; a miss falls back to a range scan on the store, which is sorted by recent interaction to support pagination. To keep write latency low, we batch updates and fan‑out the second write to an asynchronous worker. Suggestions are pre‑computed nightly using a Spark job that counts mutual friends and stores the top candidates per user. This design gives sub‑100 ms read latency for the majority of traffic and scales horizontally by sharding on user ID."
You can rehearse this answer aloud, and Call Assistant will keep your story anchored to the specific projects on your resume, ensuring you stay on point during the interview.
How to practice this
- Write the API spec on a sheet of paper, then explain each endpoint aloud. Record yourself and listen for filler words.
- Sketch the architecture without looking at notes, then compare to the diagram above. Identify any missing components.
- Run a mock interview with a peer or use Call Assistant to simulate the interview flow, focusing on answering follow‑up questions concisely.
FAQ
Q: Do I need to store the entire friend list in memory? A: No. Keep only the most active users in a cache; the rest can be fetched from the NoSQL store on demand. This balances latency and cost.
Q: How do I guarantee that
AddFriendis idempotent? A: Before writing, check if the edge already exists. If it does, return success without performing another write. This also simplifies retry logic.Q: What is the impact of eventual consistency on the user experience? A: Users might see a brief period where the new friend does not appear in their list. Most apps hide this by showing a pending state until the write propagates.
Q: Should I use a graph database instead of a key‑value store? A: Graph databases excel at complex traversals but add operational overhead. For a simple friendship service, a key‑value store with a pre‑computed suggestion layer is usually sufficient and easier to scale.
Frequently asked questions
Do I need to store the entire friend list in memory?
No. Keep only the most active users in a cache; the rest can be fetched from the NoSQL store on demand. This balances latency and cost.
How do I guarantee that AddFriend is idempotent?
Before writing, check if the edge already exists. If it does, return success without performing another write. This also simplifies retry logic.
What is the impact of eventual consistency on the user experience?
Users might see a brief period where the new friend does not appear in their list. Most apps hide this by showing a pending state until the write propagates.
Should I use a graph database instead of a key-value store?
Graph databases excel at complex traversals but add operational overhead. For a simple friendship service, a key-value store with a pre-computed suggestion layer is usually sufficient and easier to scale.
#system design#social graph#architecture#scalability#interview prep#a social graph service