Back to Categories
System Design

System Design

URL shortener, rate limiter, CAP theorem, microservices and real-time systems

17Questions

🧠 Simple Definition (Word-for-word)

Vertical: add more CPU/RAM to one machine — simple, no code changes, but limited and expensive, single point of failure.


⚡ Super Simple Line

Horizontal: add more machines — theoretically unlimited scale, resilient.


⚡ Key Details & Explanation

Vertical: add more CPU/RAM to one machine — simple, no code changes, but limited and expensive, single point of failure. Horizontal: add more machines — theoretically unlimited scale, resilient. Horizontal is hard when: state needs to be shared (sessions, file uploads — need shared Redis/S3), database writes don't scale easily horizontally (sharding adds complexity), stateful WebSocket connections tied to one server. Stateless apps scale horizontally easily — this is why stateless JWT and external state stores (Redis) matter.


🌱 Beginner Explanation

Vertical scaling is "buy bigger machine." Horizontal scaling is "add more machines." Horizontal sounds better, but becomes hard when system keeps important state in one machine's memory or local disk.

Stateless app servers are easiest to scale horizontally. Shared state usually moves to Redis, S3, or database.


🗣️ How To Explain In Interview

Vertical scaling is simpler but limited and creates a stronger single point of failure. Horizontal scaling gives better long-term scale and resilience, but shared state, database writes, and stateful connections like WebSockets make it harder.


❓ Follow-up Questions

  • Why are stateless services easier to scale?
    Any request can go to any server.

  • Why are writes harder than reads?
    Because correctness and coordination across nodes are harder.

  • How do sessions scale horizontally?
    Shared Redis session store or stateless tokens.

🧠 Simple Definition (Word-for-word)

CDN (Content Delivery Network): geographically distributed servers that cache and serve static assets (images, JS, CSS, videos) from the edge server closest to the user.


⚡ Super Simple Line

How it works: request hits CDN edge, on cache miss the edge fetches from your origin server and caches it (TTL-based).


⚡ Key Details & Explanation

CDN (Content Delivery Network): geographically distributed servers that cache and serve static assets (images, JS, CSS, videos) from the edge server closest to the user. How it works: request hits CDN edge, on cache miss the edge fetches from your origin server and caches it (TTL-based). Subsequent requests are served from the edge without hitting your origin. In Next.js: next/image automatically uses the CDN configured in Vercel, static assets in /public are CDN-served by default. Cache-Control headers control CDN caching behavior.


🌱 Beginner Explanation

CDN is basically a distributed cache in front of your origin server. Closest edge server answers request if it already has file. That reduces latency and also protects origin from repeated traffic.

Important beginner words: edge, cache hit, cache miss, TTL, origin.


🗣️ How To Explain In Interview

A CDN stores cacheable content at edge locations near users. On a cache hit, user gets content quickly from nearby server. On a cache miss, edge fetches from origin, stores it, and future requests become faster.


❓ Follow-up Questions

  • What is cache hit ratio?
    Percent of requests served directly from CDN cache.

  • Why does CDN reduce load?
    Origin does not serve every repeated asset request.

  • Can dynamic responses use CDN?
    Yes, if caching rules allow it.

🧠 Simple Definition (Word-for-word)

In a distributed system you can guarantee at most 2 of 3: Consistency (every read gets the latest write), Availability (every request gets a response), Partition Tolerance (system works despite network failures).


⚡ Super Simple Line

Since network partitions WILL happen, the real choice is C vs A during a partition.


⚡ Key Details & Explanation

In a distributed system you can guarantee at most 2 of 3: Consistency (every read gets the latest write), Availability (every request gets a response), Partition Tolerance (system works despite network failures). Since network partitions WILL happen, the real choice is C vs A during a partition. CP systems (PostgreSQL, MongoDB with majority reads): return error rather than stale data. AP systems (Cassandra, DynamoDB): return possibly stale data but remain available. Choose based on your domain: banking = CP, social feed = AP.


🌱 Beginner Explanation

CAP theorem confuses many beginners because they think it means you can freely pick any two. Better way to think:

When network breaks between nodes, do I prefer correct data or always returning a response?

That is real tradeoff. Partition tolerance is not optional in distributed systems because networks fail.


🗣️ How To Explain In Interview

CAP theorem matters only when a partition happens. At that moment, I cannot have both perfect consistency and full availability. So for something like payments I would prefer consistency, while for something like a social feed I may prefer availability.


❓ Follow-up Questions

  • What does consistency mean here?
    Every read sees latest write, not just "data validity".

  • Why is partition tolerance mandatory?
    Because network failures happen in real distributed systems.

  • Can one product mix CP and AP?
    Yes, different subsystems can make different tradeoffs.

🧠 Simple Definition (Word-for-word)

Eventual consistency: in a distributed system, all replicas will eventually converge to the same value — but at any point in time, different nodes may serve different values.


⚡ Super Simple Line

Acceptable for: social media feeds (stale posts for a few seconds is fine), shopping cart (eventual sync is ok), DNS propagation, search indexes.


⚡ Key Details & Explanation

Eventual consistency: in a distributed system, all replicas will eventually converge to the same value — but at any point in time, different nodes may serve different values. Acceptable for: social media feeds (stale posts for a few seconds is fine), shopping cart (eventual sync is ok), DNS propagation, search indexes. Not acceptable for: financial transactions (money debits), inventory counts where overselling is a problem, authentication (revoked tokens must be respected immediately).


🌱 Beginner Explanation

Eventual consistency means data may be briefly different on different replicas, but if no new writes happen, they will converge. This is often acceptable when being a few seconds behind is not a business disaster.

Good mental model: you update profile photo, one region shows new image now, another region shows old image for a few seconds, then catches up.


🗣️ How To Explain In Interview

Eventual consistency means replicas may temporarily disagree, but they converge over time. I would accept that for feeds, search indexes, and analytics, but not for bank balances or strict inventory counts.


❓ Follow-up Questions

  • Why would a system choose eventual consistency?
    To improve availability, latency, and geographic scale.

  • Where is it dangerous?
    Payments, auth revocation, strict inventory.

  • How do clients handle it?
    Show loading states, versioning, retries, or read-after-write strategies.

🧠 Simple Definition (Word-for-word)

Long polling: client makes request, server holds it open until data is available, then responds, client immediately makes new request — lots of overhead, HTTP overhead per message.


⚡ Super Simple Line

SSE (Server-Sent Events): persistent one-directional connection (server to client only) over HTTP — simple, auto-reconnect, EventSource API.


⚡ Key Details & Explanation

Long polling: client makes request, server holds it open until data is available, then responds, client immediately makes new request — lots of overhead, HTTP overhead per message. SSE (Server-Sent Events): persistent one-directional connection (server to client only) over HTTP — simple, auto-reconnect, EventSource API. WebSockets: full-duplex persistent TCP connection — both directions, lower overhead per message, more complex. Use SSE for: notifications, live feeds (server pushes only). Use WebSockets for: chat, collaborative editing, games (bidirectional real-time).


🌱 Beginner Explanation

Difference is mainly connection style and direction.

  • Long polling: repeated HTTP requests, highest overhead

  • SSE: one-way server-to-client stream

  • WebSocket: full two-way persistent connection

Simple rule: notifications/live feed often fit SSE; chat/games/collaboration need WebSockets.


🗣️ How To Explain In Interview

If server only needs to push updates to client, SSE is often simpler than WebSockets. If both client and server need to send frequent messages, like chat or multiplayer features, WebSockets are more appropriate. Long polling is older and heavier because every update still involves repeated HTTP requests.


❓ Follow-up Questions

  • Why is SSE simpler?
    Built on normal HTTP and one-directional.

  • Why not use long polling first?
    It creates more request overhead and latency.

  • Why WebSockets for chat?
    Because chat needs low-latency communication in both directions.

🧠 Simple Definition (Word-for-word)

An API gateway is the single entry point for clients in front of multiple services.


⚡ Super Simple Line

It can handle authentication, rate limiting, request routing, header normalization, logging, and sometimes response aggregation.


⚡ Key Details & Explanation

An API gateway is the single entry point for clients in front of multiple services. It can handle authentication, rate limiting, request routing, header normalization, logging, and sometimes response aggregation. This keeps cross-cutting concerns out of every service. Tradeoff: it can become a bottleneck or too smart if you push business logic into it. Keep it focused on routing and platform concerns, not domain behavior.


🌱 Beginner Explanation

API gateway is front door of distributed system. Clients talk to gateway, and gateway routes request to correct internal service. This keeps clients simpler and centralizes cross-cutting concerns.

Gateway should handle platform concerns, not business logic.


🗣️ How To Explain In Interview

I would use an API gateway as a single entry point in front of multiple services. It can centralize authentication, rate limiting, routing, logging, and header normalization. But I would avoid putting core domain logic there so it does not become an overly smart bottleneck.


❓ Follow-up Questions

  • Why not let clients call services directly?
    That increases client complexity and duplicates concerns like auth and rate limiting.

  • Can gateway aggregate responses?
    Yes, sometimes, but keep it limited.

  • Big risk of gateway?
    It can become bottleneck or single point of failure if poorly designed.

🧠 Simple Definition (Word-for-word)

Message queues decouple producers and consumers, enable async processing, and add resilience.


⚡ Super Simple Line

RabbitMQ: traditional message broker, complex routing (exchanges, bindings), good for task queues, RPC patterns, small-medium volume.


⚡ Key Details & Explanation

Message queues decouple producers and consumers, enable async processing, and add resilience. RabbitMQ: traditional message broker, complex routing (exchanges, bindings), good for task queues, RPC patterns, small-medium volume. Kafka: high-throughput event streaming platform, messages retained and replayable, consumer groups, event sourcing — best for data pipelines, analytics, millions of events/sec. Bull/BullMQ: Redis-backed job queue for Node.js — simple, great for background jobs (email, thumbnail generation), retry logic, no separate infra if you already use Redis.


🌱 Beginner Explanation

All three tools move work asynchronously, but they solve slightly different problems.

  • Kafka is event streaming with replay

  • RabbitMQ is broker-style messaging and routing

  • BullMQ is practical app job queue for Node.js

If beginner answer feels too broad, use real examples: BullMQ for sending emails, RabbitMQ for routing tasks across services, Kafka for clickstream analytics.


🗣️ How To Explain In Interview

I would pick based on workload. For simple application background jobs in a Node.js stack, BullMQ is enough. For broker-style routing between services, RabbitMQ is strong. For very high-throughput event streams and replay, Kafka is best.


❓ Follow-up Questions

  • What is replay?
    Consumers can read old retained events again.

  • What is dead-letter queue?
    Failed messages go there after retry attempts.

  • Why not direct HTTP call instead of queue?
    Queue reduces coupling and handles spikes better.

🧠 Simple Definition (Word-for-word)

Event-driven architecture is a design pattern where the flow of the application is driven by events.


⚡ Super Simple Line

Instead of directly calling functions, components communicate by emitting and listening to events.


⚡ Key Details & Explanation

Benefits include:

  • Loose coupling between components
  • Better scalability
  • Easier to extend features

In Node.js, this pattern is very common because of EventEmitter and async nature.

For example, in a notification system, one service emits an event and multiple services like email or logging can react to it independently.


🌱 Beginner Explanation

Event-driven architecture means one part of system emits event like OrderPlaced, and other parts react to it. Producer does not need to know every consumer directly.

That makes system easier to extend. New service can subscribe later without changing original producer.


🗣️ How To Explain In Interview

In event-driven architecture, services communicate by publishing and consuming events rather than calling each other directly for every action. This reduces coupling, improves scalability, and makes it easier to add new consumers like notifications, analytics, or auditing.


❓ Follow-up Questions

  • What is main benefit?
    Loose coupling between services.

  • What is challenge?
    Debugging and guaranteeing delivery/order can be harder.

  • Good example event?
    OrderPlaced consumed by email, billing, analytics.

🧠 Simple Definition (Word-for-word)

Microservices: split an app into independently deployable services, each owning its data and running in its own process.


⚡ Super Simple Line

Benefits: independent scaling, independent deployment, tech heterogeneity, team autonomy.


⚡ Key Details & Explanation

Microservices: split an app into independently deployable services, each owning its data and running in its own process. Benefits: independent scaling, independent deployment, tech heterogeneity, team autonomy. Problems they introduce: distributed system complexity (network failures, latency), service discovery, distributed tracing, data consistency across services (no ACID transactions), API contracts between teams, operational overhead (many deployments, many logs). Start with a monolith, extract services only when there's a clear scaling or team boundary reason.


🌱 Beginner Explanation

Microservices are not automatically "better architecture." They trade simplicity for team independence and scaling flexibility. A monolith is one deployable application. Microservices split that into many networked applications.

When beginners answer this, strongest line is: microservices solve some problems by creating new problems.


🗣️ How To Explain In Interview

Microservices help when teams or scaling needs are large enough to justify separate services, deployments, and ownership. But they also create network failures, tracing challenges, data consistency issues, and more operational work, so I would not start with them unless there is a clear reason.


❓ Follow-up Questions

  • Why start with monolith?
    Faster development, simpler deployment, easier debugging.

  • What replaces one big DB transaction?
    Saga or compensation patterns.

  • What monitoring becomes important?
    Distributed tracing, centralized logs, service metrics.

🧠 Simple Definition (Word-for-word)

To handle a sudden spike in traffic: I would first ensure that my infrastructure can scale (using cloud services like AWS or GCP).


⚡ Super Simple Line

Implement auto-scaling to add more servers as needed.


⚡ Key Details & Explanation

  • I would first ensure that my infrastructure can scale (using cloud services like AWS or GCP).
  • Implement auto-scaling to add more servers as needed.
  • Use a load balancer to distribute traffic evenly across servers.
  • Implement caching to reduce database load.
  • And if necessary, I would also consider using a CDN to serve static assets faster.

This way, I can maintain performance and prevent downtime during traffic spikes.


🌱 Beginner Explanation

Sudden 100x traffic means answer should be structured, not random.

  1. Protect system first

  2. Scale stateless layers

  3. Reduce load on bottlenecks

  4. Gracefully degrade if needed

This is not only about adding servers. Usually real bottleneck is database, cache, queue, or third-party dependency.


🗣️ How To Explain In Interview

My first step is to stabilize the system: rate limit abusive traffic, cache aggressively, and scale stateless app servers behind a load balancer. Then I would protect the database with read replicas, query optimization, and queue-based async work. If needed, I would degrade noncritical features temporarily so core user flows stay alive.


❓ Follow-up Questions

  • What do you protect first?
    Database and core critical paths.

  • Why graceful degradation?
    It keeps checkout/login/core flows alive by disabling nonessential features.

  • What metrics matter during spike?
    Latency, error rate, CPU, DB connections, cache hit rate, queue depth.

🧠 Simple Definition (Word-for-word)

A Rate Limiter is a system component that limits the number of requests a client can make within a given timeframe to protect API resources from abuse or overloading. The Token Bucket algorithm maintains a bucket filled with tokens at a constant rate; requests consume tokens, allowing bursts of traffic. The Sliding Window Counter tracks request timestamps within a sliding window interval, offering strict rate limits with low memory overhead.


📊 Comparison of Rate Limiting Algorithms

AlgorithmTraffic BurstsMemory UsageImplementation Complexity
Token Bucket✅ AllowedLow (stores integer)Easy
Leaky Bucket❌ Smoothed outLow (FIFO queue)Medium
Sliding Window Log❌ Strict limit⚠️ High (stores all timestamps)Hard
Sliding Window Counter❌ Strict limitLow (stores counters)Medium

🧪 Redis Implementation Concept

Redis is commonly used to build distributed rate limiters because operations are atomic:

// Sliding Window Counter using Redis sorted set (ZSET)
async function isRateLimited(userId) {
  const now = Date.now();
  const windowMs = 60000; // 1 minute
  const maxRequests = 100;
  
  const key = `rate_limit:${userId}`;
  
  await redis.multi()
    .zremrangebyscore(key, 0, now - windowMs) // Remove old timestamps
    .zcard(key) // Count remaining request logs
    .zadd(key, now, now) // Log current request
    .expire(key, 60)
    .exec();
}

⚡ One-line Interview Answer

Rate limiters protect APIs using algorithms like Token Bucket, which permits momentary bursts of traffic, or Sliding Window Counter, which strictly throttles traffic with low memory footprints.


🌱 Beginner Explanation

Rate limiter protects system from abuse and sudden spikes. First decide what you are limiting: per IP, per user, per API key, or per endpoint. Then decide behavior.

  • Token bucket: good when small bursts are okay

  • Sliding window: good when you want stricter fairness

In practice, Redis is common because you need one shared counter store across many app servers.


🗣️ How To Explain In Interview

I would store rate-limit state in Redis so all app servers share the same counters. If I want to allow bursts, I would use token bucket. If I want stricter control over requests in the last N seconds, I would use a sliding-window approach.


❓ Follow-up Questions

  • Why not in-memory counters?
    Because multiple app servers would each have different counts.

  • What response code is common?
    429 Too Many Requests.

  • What headers can you return?
    Remaining quota, reset time, retry-after.

🧠 Simple Definition (Word-for-word)

Core: generate a short code (6-8 chars) mapped to a long URL.


⚡ Super Simple Line

Schema: {id, shortCode, longUrl, userId, createdAt, clickCount}.


⚡ Key Details & Explanation

Core: generate a short code (6-8 chars) mapped to a long URL. Schema: {id, shortCode, longUrl, userId, createdAt, clickCount}. Shortcode generation: base62 encoding of auto-incremented ID, or random string with collision check. API: POST /shorten → returns shortUrl; GET /:code → 301 redirect to longUrl. Scalability: cache hot URLs in Redis (most URLs are accessed rarely — 80/20 rule), CDN for the redirect service, DB sharding by shortCode hash for massive scale. Analytics: async queue for click tracking.


🌱 Beginner Explanation

Think of this as a very fast dictionary.

shortCode -> longUrl

When user creates short URL, system stores mapping. When someone opens short URL, system finds original URL and redirects. Since redirects happen far more often than URL creation, this is a read-heavy system. That is why caching is so important.

Start simple in interviews:

  • API for create

  • DB table for mapping

  • Redis for hot links

  • Async analytics


🗣️ How To Explain In Interview

I would model a URL shortener as a read-heavy key-value lookup system. The write path creates a unique short code and stores it with the long URL. The read path looks up the short code and returns a redirect, ideally from Redis first and then the database. At scale, I would separate analytics from the redirect path so user latency stays low.


❓ Follow-up Questions

  • How do you prevent collisions?
    Use a unique index on shortCode and retry generation if insert fails.

  • Why cache here?
    Because redirect traffic is much higher than create traffic.

  • 301 or 302?
    301 for permanent redirects, 302 if destination may change.

🧠 Simple Definition (Word-for-word)

Avoid sending large files through your server.


⚡ Super Simple Line

Pattern: client requests a presigned URL from your API, your API calls S3 to generate a presigned URL, client uploads directly to S3 bypassing your server.


⚡ Key Details & Explanation

Avoid sending large files through your server. Pattern: client requests a presigned URL from your API, your API calls S3 to generate a presigned URL, client uploads directly to S3 bypassing your server. For very large files (>100MB): multipart upload — split into chunks (5MB+ each), upload each chunk with its own presigned URL, complete multipart upload when all chunks arrive. Your API just coordinates metadata. Resumability: track which chunks are uploaded, resume from where it failed.


🌱 Beginner Explanation

Main beginner mistake is sending entire file through backend server. That wastes app-server bandwidth and makes scaling expensive. Better pattern:

Client asks backend for permission -> Backend returns presigned upload URL -> Client uploads directly to object storage -> Backend stores metadata only

For very large files, multipart upload is important because failed chunks can be retried instead of restarting whole upload.


🗣️ How To Explain In Interview

I would keep the backend out of the file data path. The backend authenticates the user and creates a presigned upload session, but the client uploads directly to S3 or similar object storage. For large files, I would use multipart upload so uploads can resume and only failed chunks need retrying.


❓ Follow-up Questions

  • Why direct upload?
    Less backend load, cheaper bandwidth, easier scaling.

  • How do you support resume?
    Multipart upload plus chunk tracking.

  • How do you secure it?
    Short-lived presigned URLs and server-side validation of size/type.

🧠 Simple Definition (Word-for-word)

Frontend should debounce input and cancel stale requests.


⚡ Super Simple Line

Backend options depend on scale: SQL prefix search for simple cases, Elasticsearch/OpenSearch for large-scale ranking, typo tolerance, and advanced relevance.


⚡ Key Details & Explanation

Frontend should debounce input and cancel stale requests. Backend options depend on scale: SQL prefix search for simple cases, Elasticsearch/OpenSearch for large-scale ranking, typo tolerance, and advanced relevance. Return small payloads quickly, usually top 5-10 suggestions. Cache hot queries, track analytics for no-result searches, and rank by a combination of prefix match, popularity, and recent behavior where relevant.


🌱 Beginner Explanation

Autocomplete is about giving a few good suggestions very quickly. Fast response matters more than returning huge result sets.

Start small:

  • Debounce input on frontend

  • Cancel stale requests

  • Return top 5-10 suggestions

For simple systems, SQL prefix search can work. For advanced ranking and typo tolerance, search engine like Elasticsearch/OpenSearch is better.


🗣️ How To Explain In Interview

I would optimize for low latency and relevance. The frontend should debounce and cancel stale requests. Backend can start with prefix search in SQL for small scale, but for typo tolerance, ranking, and large query volume, I would move to Elasticsearch or OpenSearch and cache hot queries.


❓ Follow-up Questions

  • Why debounce?
    Prevents request on every keystroke.

  • Why cancel stale requests?
    So old slow responses do not overwrite newer user input.

  • What ranking signals matter?
    Prefix match, popularity, recency, user behavior.

🧠 Simple Definition (Word-for-word)

WebSockets for persistent connections — but can't hold 1M open connections on one server.


⚡ Super Simple Line

Solution: use a pub/sub layer (Redis Pub/Sub or Kafka).


⚡ Key Details & Explanation

WebSockets for persistent connections — but can't hold 1M open connections on one server. Solution: use a pub/sub layer (Redis Pub/Sub or Kafka). Each notification server subscribes to channels for its connected users. When a notification is generated, publish to the user's channel — the server holding that user's connection delivers it. Use a message queue for delivery guarantees and retry. Store undelivered notifications in DB for users who are offline. Send push notifications (FCM/APNs) for mobile.


🌱 Beginner Explanation

Notification system has two jobs:

  1. Store notification so it is not lost

  2. Deliver notification fast to online users

Do not depend only on WebSocket. If user is offline, WebSocket cannot help. So store notification in DB first, then push in real time if user is connected. For 1M users, many WebSocket servers sit behind load balancer, and a pub/sub layer helps route messages to correct server.


🗣️ How To Explain In Interview

I would separate notification persistence from delivery. First, store notification durably in the database. Then publish an event so the WebSocket server holding that user's connection can deliver it in real time. If the user is offline, the notification remains unread in storage and can be fetched later.


❓ Follow-up Questions

  • Why not just WebSockets?
    Because offline users would lose notifications.

  • Why use Redis Pub/Sub or Kafka?
    Because user connections are spread across many servers.

  • How do mobile users get alerts?
    Use push providers like FCM or APNs.

🧠 Simple Definition (Word-for-word)

For a chat system, I would start with conversations, participants, and messages as the core data model.


⚡ Super Simple Line

When a user sends a message, the backend should validate the user, save the message to the database first, and then deliver it in real time to online users through WebSockets.


⚡ Key Details & Explanation

For a chat system, I would start with conversations, participants, and messages as the core data model. When a user sends a message, the backend should validate the user, save the message to the database first, and then deliver it in real time to online users through WebSockets. If the recipient is offline, the message stays stored and can be loaded when they reconnect, and the system can also send a push notification. Message history should use cursor pagination because chat history can become large and users usually load older messages gradually. The system also needs delivery status, read receipts if required, and ordering by a reliable timestamp or sequence number. At scale, WebSocket servers need shared state through Redis Pub/Sub, Kafka, or another messaging layer so a message can reach users connected to different servers. The most important tradeoff is reliability versus complexity: for simple chat, saving messages and pushing live updates is enough, but for large-scale chat, ordering, retries, and multi-server delivery become the hard parts.


🌱 Beginner Explanation

Chat system has both storage problem and delivery problem.

  • Storage: save messages reliably and paginate history

  • Delivery: send instantly to online users, later to offline users

Good beginner design: save message first, then push through WebSocket. That avoids losing chat messages if delivery fails.


🗣️ How To Explain In Interview

I would save each message durably before attempting real-time delivery. Online users get it immediately through WebSockets, while offline users fetch it later from storage and may receive a push notification. At scale, multiple socket servers need shared pub/sub so users connected to different servers still receive messages.


❓ Follow-up Questions

  • Why cursor pagination for history?
    Chat history grows large and users load older messages gradually.

  • How do you handle ordering?
    Use server timestamps or sequence IDs per conversation.

  • What about delivery receipts?
    Store message status like sent, delivered, read.

🧠 Simple Definition (Word-for-word)

Two main approaches: Operational Transformation (OT) — transforms concurrent operations to be compatible (used by Google Docs); CRDT (Conflict-free Replicated Data Types) — data structures designed to merge automatically without conflict (used by Figma, Notion).


⚡ Super Simple Line

For a simpler system: use Yjs library (CRDT-based), connect via WebSocket (Socket.io or native), broadcast operations to all connected clients.


⚡ Key Details & Explanation

Two main approaches: Operational Transformation (OT) — transforms concurrent operations to be compatible (used by Google Docs); CRDT (Conflict-free Replicated Data Types) — data structures designed to merge automatically without conflict (used by Figma, Notion). For a simpler system: use Yjs library (CRDT-based), connect via WebSocket (Socket.io or native), broadcast operations to all connected clients. Persist: save document state to DB on debounced changes or a dedicated save action. The hard part is handling concurrent edits and cursor positions.


🌱 Beginner Explanation

Collaborative editors are hard because many users may edit same text at same time. System must merge edits safely and keep everyone in sync. That is why answers mention OT and CRDT.

If beginner, it is perfectly okay in interview to say: I would use proven library like Yjs instead of inventing OT/CRDT algorithm myself.


🗣️ How To Explain In Interview

I would use WebSockets for real-time communication and a proven conflict-resolution model like CRDT through Yjs. Clients send operations, server broadcasts them, and document state is persisted periodically. Main challenges are concurrent edits, cursor positions, and offline edits.


❓ Follow-up Questions

  • Why not save full document on every keystroke?
    Too much bandwidth and conflict risk.

  • What is CRDT advantage?
    Concurrent changes can merge automatically.

  • Why WebSocket here?
    Need fast bidirectional low-latency updates.