System Design Interview Questions
Ace your system design interviews with the most asked HLD and LLD questions covering scalability, distributed systems, design patterns, and real-world architecture problems for freshers and experienced engineers.
Introduction to System Design
1. What is system design and why is it important?
System design is the process of defining the architecture, components, modules, interfaces, and data flows of a system to satisfy specified requirements. It translates business goals into a concrete engineering blueprint that can be implemented, scaled, and maintained.
Scalability: Ensures the system can handle growth in users, data, and traffic without redesign.
Reliability: The system continues to work correctly even when components fail.
Maintainability: Code and infrastructure are easy to understand, extend, and operate.
Cost-efficiency: Guides informed tradeoffs between performance, infrastructure spend, and engineering complexity.
System design interviews evaluate whether a candidate can think at scale, communicate architecture decisions clearly, and reason about tradeoffs — skills critical for senior and staff engineers.
2. What is the difference between HLD and LLD?
HLD (High-Level Design) and LLD (Low-Level Design) address different levels of abstraction in the design process.
HLD: Defines the overall architecture — which services exist, how they communicate, which databases and caches to use, and how the system scales. Audience: architects and senior engineers. Output: system architecture diagram, API contracts, data flow diagrams.
LLD: Defines the detailed implementation of each component — class diagrams, data models, algorithms, method signatures, and design patterns applied. Audience: the engineering team that will implement it. Output: class diagrams, sequence diagrams, ER diagrams.
HLD answers 'what to build and how the parts connect'; LLD answers 'how to implement each part in code'. Both are evaluated in interviews, often in the same question for case studies.
3. What are functional and non-functional requirements?
Clarifying requirements is the mandatory first step of every system design interview. Requirements fall into two categories.
Functional requirements: What the system must do. Core features and user-facing behaviours. Example for a URL shortener: create short URLs, redirect to original URL, support custom aliases, set expiry.
Non-functional requirements (NFRs): How well the system must perform. Include availability (99.99% uptime), latency (P99 < 100ms), throughput (10,000 RPS), durability (no data loss), consistency (eventual vs. strong), and scalability (100M users).
NFRs drive almost every architectural decision — database choice, caching strategy, replication factor, and consistency model. Always clarify and quantify NFRs before drawing any diagrams.
4. What is the difference between scalability and performance?
These terms are closely related but address different concerns.
Performance: How fast the system handles a single request right now — measured as latency (response time in ms) and throughput (requests per second at current load). A fast single server is high performance.
Scalability: The ability to maintain acceptable performance as load increases — 10x users, 100x data. A scalable system grows gracefully without requiring a complete redesign.
A system can be fast but not scalable (a single powerful server), or scalable but not fast (a distributed system with high coordination overhead). The goal is to design systems that are both.
5. What is vertical scaling vs horizontal scaling?
Scaling is the process of adding resources to handle increased load. There are two fundamental strategies.
Vertical Scaling (Scale Up): Add more power to an existing machine — more CPU cores, RAM, or faster SSDs. Simple with no code changes, but has a hard hardware ceiling, is expensive at the top end, and creates a single point of failure. Example: upgrading a DB server from 32GB to 256GB RAM.
Horizontal Scaling (Scale Out): Add more machines and distribute load across them using a load balancer. Theoretically unlimited, cost-effective with commodity hardware, and fault-tolerant — but requires stateless services and adds operational complexity. Example: running 50 identical web servers behind an NGINX load balancer.
All major internet systems (Google, Amazon, Netflix) rely on horizontal scaling. Stateless services are trivially horizontally scalable. Stateful services (databases) need sharding or replication to scale horizontally.
6. What is the difference between latency and throughput?
Latency and throughput are the two primary metrics for system performance.
Latency: Time to process a single request end-to-end. Measured in milliseconds. P50 (median), P95, and P99 percentiles are standard reporting metrics. Users notice latency above 100–200ms. Sources: network round trips, DB queries, serialization, processing time.
Throughput: Number of requests processed per unit of time (RPS, TPS, QPS). Represents the system's capacity. Limited by the slowest component in the pipeline (bottleneck). Batching and parallelism increase throughput.
There is often a tradeoff: batching increases throughput but adds latency. Prioritise low latency for user-facing APIs (< 100ms P99) and high throughput for background data pipelines.
7. What is the difference between availability and consistency?
Availability and consistency are two of the three properties in the CAP theorem and represent a fundamental tradeoff in distributed systems.
Availability: Every request receives a response — even if some nodes are down. The system is always operational. Example: A DNS server that returns cached (possibly stale) results even during a partial outage.
Consistency: Every read reflects the most recent write. All nodes see the same data at the same time. Example: A bank balance — reading it from any replica must always return the current value.
Eventual Consistency: A middle ground — replicas may be temporarily out of sync but will converge to the same state given enough time. Used by Amazon DynamoDB, Cassandra, and most social media feeds.
Choose strong consistency for financial transactions, inventory management, and any system where stale data causes real harm. Choose high availability with eventual consistency for social feeds, product catalogs, and analytics.
8. What is the CAP theorem? Explain with real examples.
The CAP theorem (Brewer's theorem) states that a distributed system can guarantee at most two of the following three properties simultaneously: Consistency, Availability, and Partition Tolerance. Since network partitions are inevitable in any distributed system, engineers must choose between Consistency and Availability when a partition occurs.
CP systems (Consistency + Partition Tolerance): Return an error rather than stale data during a partition. Examples: ZooKeeper (leader election), HBase, etcd, MongoDB (strong consistency mode). Use for: distributed locks, config management, financial ledgers.
AP systems (Availability + Partition Tolerance): Return possibly stale data rather than an error during a partition. Examples: Cassandra, DynamoDB, CouchDB. Use for: social feeds, product catalogs, DNS, shopping carts.
CA systems: Only possible when there are no partitions — i.e., a single-node database. Traditional RDBMS (PostgreSQL, MySQL) on a single server is CA. Not viable for truly distributed systems.
In practice the choice is not binary — modern systems like Cassandra allow tunable consistency (quorum reads/writes) so you can trade availability for consistency per operation depending on business needs.
9. What is the difference between ACID and BASE in databases?
ACID and BASE represent two philosophies for database transaction guarantees, each suited to different workload types.
ACID (Atomicity, Consistency, Isolation, Durability): Every transaction either fully succeeds or is fully rolled back (Atomicity). Data always moves from one valid state to another (Consistency). Concurrent transactions do not interfere with each other (Isolation). Committed data persists even after crashes (Durability). Used by: PostgreSQL, MySQL, Oracle, SQL Server.
BASE (Basically Available, Soft state, Eventual consistency): The system is basically available (AP per CAP). State may be temporarily inconsistent (Soft state). Replicas eventually converge (Eventual consistency). Used by: Cassandra, DynamoDB, MongoDB (default mode), Riak.
Use ACID for banking, order management, and any workflow where partial updates are catastrophic. Use BASE for high-scale read-heavy workloads (user activity logs, social data, analytics) where eventual consistency is acceptable.
10. What is the difference between monolithic and microservices architecture?
Monolithic and microservices architectures represent two fundamentally different ways of structuring an application, each with distinct tradeoffs.
Monolith: All components (UI, business logic, data access) are bundled into and deployed as a single unit. Pros: simple to develop, test, and deploy initially; low network overhead; easy debugging. Cons: hard to scale individual components; one bug can crash the entire app; deployment risk grows with codebase size.
Microservices: The application is split into small, independently deployable services (e.g., User Service, Order Service, Payment Service), each owning its own database and communicating via REST or gRPC or message queues. Pros: independent scaling, deployment, and technology choice per service; fault isolation. Cons: distributed system complexity (network latency, service discovery, distributed tracing), operational overhead.
The rule of thumb: start with a well-structured monolith, then extract microservices along clear domain boundaries as the team and system grow. Netflix, Uber, and Amazon all migrated from monoliths to microservices over several years.
11. What is a load balancer and how does it work?
A load balancer is the single entry point that distributes incoming network traffic across multiple backend servers, preventing any single server from becoming a bottleneck and enabling horizontal scaling.
Round Robin: Requests distributed to servers in a circular sequence. Simple, effective when all servers have equal capacity.
Least Connections: Routes to the server with the fewest active connections. Better for variable-length request processing.
IP Hash: Same client IP always routes to the same server (sticky sessions). Required when session state is stored server-side.
Layer 4 vs Layer 7: L4 load balancers route at TCP/UDP level (faster). L7 load balancers inspect HTTP content and can route by URL path, headers, or cookies (smarter, used for API gateways).
Health checks: Load balancers continuously probe backends and automatically remove unhealthy servers from rotation.
Popular load balancers: NGINX, HAProxy, AWS ELB/ALB. A load balancer itself can be a SPOF — deploy in a pair (active-passive) with a floating IP for HA.
12. What is caching? Explain cache strategies and eviction policies.
Caching stores copies of frequently accessed data in a fast-access layer (RAM) so future requests are served without hitting the slower origin (database, disk, or external API). It is the single most impactful technique for reducing latency and database load.
Cache-Aside (Lazy Loading): On cache miss, application fetches from DB, stores in cache, and returns. Most common. Stale data risk on writes unless TTL or explicit invalidation is used.
Write-Through: Every write goes to cache and DB simultaneously. Cache is always fresh. Adds write latency.
Write-Behind (Write-Back): Writes go to cache and are asynchronously flushed to DB. Fastest writes, but data can be lost on cache crash.
Eviction policies: LRU (Least Recently Used) — evict the item not accessed longest. LFU (Least Frequently Used) — evict the item accessed fewest times. TTL — items expire after a fixed time regardless of access.
Redis supports strings, hashes, sorted sets, lists, and pub/sub — making it far more versatile than Memcached (strings only). Cache invalidation ('when to invalidate stale data') is the hardest problem in caching.
13. When would you choose SQL over NoSQL and vice versa?
Database selection is one of the most critical HLD decisions and depends on data model, access patterns, consistency requirements, and scale.
Choose SQL when: Data is structured and relational (users, orders, products). You need ACID transactions (payments, inventory). Complex joins and aggregations are required. Examples: PostgreSQL, MySQL.
Choose NoSQL when: You need horizontal scalability beyond what a single SQL node can handle. The schema is flexible or document-like. You're storing time-series, graphs, or key-value data. Examples: MongoDB (documents), Cassandra (wide-column, time-series), Redis (key-value, cache), DynamoDB (key-value at massive scale), Neo4j (graph).
Polyglot persistence: Most large systems use both. SQL for transactional data (orders, payments) and NoSQL for high-scale data (activity logs, session storage, product catalog). This is the standard at Amazon, Netflix, and Uber.
The question is not SQL vs NoSQL but rather matching the right database to each data access pattern. A mature system will typically use 3–5 different databases for different purposes.
14. What is a CDN and how does it improve performance?
A Content Delivery Network (CDN) is a geographically distributed network of edge servers (Points of Presence — PoPs) that cache and serve static content from locations close to end users, dramatically reducing latency and origin server load.
Pull CDN: On the first request for a resource, the edge fetches it from the origin and caches it. Subsequent requests are served from the edge until the TTL expires. Best for: images, CSS, JS, static HTML.
Push CDN: You proactively push content to CDN edges. Content is available at edges before the first user request. Best for: large files known in advance (game downloads, software releases, video assets).
Dynamic content acceleration: Modern CDNs (Cloudflare, AWS CloudFront) can accelerate dynamic requests by maintaining persistent connections to origins and optimising TCP routing.
Cache invalidation: CDN content is invalidated either by TTL expiry or explicit purge API calls when content is updated.
CDNs reduce origin load by 60–90%, decrease page load times globally, and improve availability. They also provide DDoS mitigation at the edge as a bonus.
15. What is a message queue and why is it used?
A message queue is a middleware component that enables asynchronous communication between services by allowing producers to publish messages and consumers to process them independently. Messages are stored durably until consumed.
Decoupling: Producer and consumer services are independent — the producer does not wait for the consumer to process the message. If the consumer is down, messages queue up and are processed when it recovers.
Load levelling: Absorbs traffic spikes. An order service can accept 100,000 orders/minute and a downstream email service processes them at 1,000/minute without data loss.
Fan-out: A single published message can be consumed by multiple independent services (notification service, analytics service, inventory service) without coupling them to the producer.
Popular choices: Kafka (high-throughput, replay, distributed log), RabbitMQ (traditional broker, routing), Amazon SQS (managed, simple), Google Pub/Sub (serverless, auto-scaling).
Message queues are the backbone of event-driven microservice architectures. Any time two services need to communicate and it does not need to be synchronous, a message queue should be considered.
16. What is consistent hashing and why is it used in distributed systems?
Consistent hashing is a technique for distributing data across nodes in a way that minimises data movement when nodes are added or removed. It is the foundation of distributed caches, key-value stores, and load balancers.
Hash ring: Both nodes and keys are hashed onto a circular ring (0 to 2^32). A key is assigned to the first node found by moving clockwise from the key's position on the ring.
Minimal remapping: When a node is added or removed, only the keys on the arc between the new/removed node and its neighbour need remapping — on average K/N keys (K = total keys, N = nodes), not all keys as in modulo hashing.
Virtual nodes (vnodes): Each physical node is assigned multiple positions on the ring to improve load distribution and handle heterogeneous node capacities. Used by DynamoDB and Cassandra.
1import hashlib
2from sortedcontainers import SortedDict
3
4class ConsistentHashRing:
5 def __init__(self, vnodes: int = 150):
6 self.vnodes = vnodes
7 self.ring: SortedDict = SortedDict()
8 self.nodes: set = set()
9
10 def _hash(self, key: str) -> int:
11 return int(hashlib.md5(key.encode()).hexdigest(), 16)
12
13 def add_node(self, node: str):
14 self.nodes.add(node)
15 for i in range(self.vnodes):
16 vkey = self._hash(f"{node}:vnode:{i}")
17 self.ring[vkey] = node
18
19 def remove_node(self, node: str):
20 self.nodes.discard(node)
21 self.ring = SortedDict((k, v) for k, v in self.ring.items() if v != node)
22
23 def get_node(self, key: str) -> str:
24 h = self._hash(key)
25 idx = self.ring.bisect_left(h)
26 if idx == len(self.ring):
27 idx = 0
28 return self.ring.peekitem(idx)[1]
29
30ring = ConsistentHashRing()
31for server in ["server-A", "server-B", "server-C"]:
32 ring.add_node(server)
33
34for key in ["user:1001", "order:42", "product:99"]:
35 print(f"{key} → {ring.get_node(key)}")
36Consistent hashing is used by Amazon DynamoDB, Apache Cassandra, Memcached clusters, and Akamai CDN for traffic distribution. It is a critical concept for any distributed system design question.
17. What is database sharding and what are the common strategies?
Database sharding is the technique of horizontally partitioning a database into smaller, independent pieces called shards. Each shard holds a subset of the data and lives on a separate database server, enabling the data tier to scale horizontally.
Range-based sharding: Rows are partitioned by a key range (e.g., user IDs 1–1M on shard 1, 1M–2M on shard 2). Simple to implement but can cause hot spots if the distribution is uneven.
Hash-based sharding: Apply a hash function to the shard key (e.g., user_id % N) to determine the shard. Distributes data evenly but makes range queries inefficient. Adding/removing shards requires rehashing.
Directory-based sharding: A lookup service (shard map) stores the mapping from key to shard. Flexible but adds an extra network hop and the lookup service itself can be a bottleneck.
Challenges: Cross-shard joins are expensive or impossible. Re-sharding as the system grows is complex. Shard hotspots (celebrity problem) must be handled with extra logic.
Instagram sharded their PostgreSQL by user ID using consistent hashing. Sharding is a last resort — exhaust vertical scaling, read replicas, and caching first before sharding.
18. What is database replication? Explain leader-follower and leader-leader.
Database replication maintains copies of data on multiple nodes for fault tolerance and read scalability.
Leader-Follower (Master-Replica): All writes go to the leader. The leader replicates changes to follower nodes. Followers serve read-only queries. On leader failure, a follower is promoted (failover). Pros: simple, strong consistency for writes. Cons: write bottleneck on the single leader, replication lag causes followers to serve stale reads.
Leader-Leader (Multi-Master): Multiple nodes accept writes. Changes are replicated bidirectionally. Pros: no single write bottleneck, geo-distributed writes. Cons: conflict resolution for concurrent writes is complex — requires last-write-wins or vector clocks.
Synchronous vs asynchronous replication: Synchronous — leader waits for follower acknowledgment before confirming the write (no data loss, higher latency). Asynchronous — leader confirms immediately and replicates in background (lower latency, risk of data loss if leader crashes before replication).
Most RDBMS deployments use leader-follower replication with asynchronous replication for performance and a semi-synchronous mode for the designated failover follower for safety.
19. What is an API Gateway and what cross-cutting concerns does it handle?
An API Gateway is the single entry point for all client requests in a microservices architecture. It handles cross-cutting concerns centrally so that individual microservices do not each have to implement them.
Request routing: Routes requests to the correct downstream microservice based on path, method, or headers.
Authentication and authorisation: Validates JWT tokens / API keys and enforces access control before forwarding the request.
Rate limiting and throttling: Enforces per-client or per-endpoint request limits to protect backend services.
Request/response transformation: Translates between protocols (REST ↔ gRPC), modifies headers, or aggregates multiple service calls (BFF pattern).
Circuit breaking and retries: Detects failing downstream services and stops forwarding requests to them, returning cached fallback responses to prevent cascading failures.
Observability: Centralised logging, distributed tracing (via trace IDs), and metrics collection across all API calls.
Popular API Gateways: AWS API Gateway, Kong, NGINX, Apigee, Traefik. The gateway must itself be highly available and low-latency as it is in the critical path of every request.
20. What is service discovery and why is it needed in microservices?
In a microservices system with dozens of services, each potentially running on hundreds of dynamically assigned container instances, hardcoding IP addresses is impossible. Service discovery solves this by letting services find each other at runtime.
Client-side discovery: The client queries a Service Registry (e.g., Eureka, Consul, etcd) to get the list of available instances, then load-balances requests itself. Used by Netflix Eureka.
Server-side discovery: The client sends the request to a router/load balancer, which queries the registry and forwards to the correct instance. The client knows nothing about the registry. Used by AWS ELB + ECS, Kubernetes kube-proxy.
Health integration: Service registries integrate with health checks — unhealthy instances are automatically deregistered so traffic is never routed to them.
In Kubernetes, service discovery is built in via DNS — each Service gets a stable DNS name regardless of which pod IPs are behind it. Consul and etcd are the standard choices for non-Kubernetes environments.
21. What is Low-Level Design (LLD) and what does it involve?
Low-Level Design is the detailed component-level design that specifies exactly how each part of a system will be implemented. While HLD defines which services exist and how they communicate, LLD defines the class structures, algorithms, data models, and design patterns within each service.
Class diagrams: Classes, their attributes, methods, and relationships (inheritance, composition, association, aggregation).
Sequence diagrams: The order of method calls and messages between objects for a specific use case (e.g., 'user places order').
Design patterns: Identifying and applying proven solutions (Singleton, Factory, Observer, Strategy, etc.) to recurring design problems.
SOLID principles: Ensuring the design is maintainable, testable, and extensible.
State machines: Modelling objects with complex lifecycle states (Order: PLACED → CONFIRMED → SHIPPED → DELIVERED).
LLD interviews expect you to write actual code (in Python, Java, or any OOP language), not just draw boxes. The interviewer evaluates clarity of abstraction, correct use of patterns, and adherence to SOLID principles.
22. What are SOLID principles?
The SOLID principles are five guidelines in object-oriented design that help you build systems that are easier to understand, extend, and maintain.
S - Single Responsibility Principle: A class should have one, and only one, reason to change.
A class or module should have only one responsibility. If something changes, only one part of your code should need to change.O - Open/Closed Principle: Entities should be open for extension, but closed for modification.
Software entities (classes, modules, functions, etc.) should be open for extension (adding new behavior), but closed for modification (changing existing behavior).L - Liskov Substitution Principle: Objects of a subclass should be usable without breaking the program when substituted for their parent class.
This means that every subclass or derived class should be substitutable for their base or parent class.I - Interface Segregation Principle: Clients should not be forced to depend on methods they do not use.
Don’t force a class to implement things it doesn’t need. Instead of one large, general interface, use multiple smaller, more specific interfaces.D - Dependency Inversion Principle: Entities must depend on abstractions, not on concretions.
High-level modules should not depend on low-level modules. Both should depend on abstractions.
SOLID violations are the root cause of most maintainability problems: god classes (SRP violation), feature flags everywhere (OCP violation), and hard-to-test code (DIP violation). Mentioning specific violations and their fixes in an LLD interview is highly valued.
23. What are design patterns? Explain the three main categories.
Design patterns are well-proven solutions for solving recurring software design problems. They became widely recognized due to the famous 'Gang of Four' GoF book 'Design Patterns: Elements of Reusable Object-Oriented Software' in 1994 and are subdivided into three groups:
Creational Patterns: Manage the process of object creation. Separate the object creation from the client code that uses this object. Examples: Singleton (one instance), Factory Method (delegation of construction to the subclass), Abstract Factory (object families), Builder (building complex object in parts), Prototype (copying existing objects).
Structural Patterns: Combine classes and objects to form more complex structures. Examples: Adapter (interface conversion), Decorator (dynamic addition of responsibilities), Facade (simplifying interface to complex subsystem), Composite (tree structures), Proxy (controlling object access), Flyweight (sharing states), Bridge (de-coupling abstraction from implementation).
Behavioral Patterns: Capture the ways of how objects communicate and interact among themselves. Examples: Observer (notification on change of state), Strategy (changing algorithm dynamically), Command (packaging of requests into objects), State (behavior depends on state), Chain of Responsibility (passing a request to a handler chain), Template method (defining the skeleton of an algorithm).
The most asked patterns in LLD interviews: Singleton, Factory, Observer, Strategy, Decorator, Command, State, and Builder. Know their intent, structure, and at least one real-world code example for each.
24. When should you prefer composition over inheritance?
The rule of thumb is 'favour composition over inheritance' (GoF principle). Inheritance creates a tight compile-time coupling between parent and child classes and can violate LSP when the 'is-a' relationship is not truly valid.
Use inheritance when: There is a genuine 'is-a' relationship (Dog IS-A Animal, Circle IS-A Shape). The child class truly extends (not restricts) the parent's behaviour. You are modelling domain hierarchies with shared polymorphic behaviour.
Use composition when: The relationship is 'has-a' (Car HAS-A Engine). Behaviour needs to be swappable at runtime (Strategy pattern). You want to avoid the fragile base class problem where a change in the parent breaks all children.
1// ✗ Inheritance — wrong model
2import java.util.*;
3
4class BadStack extends ArrayList<Integer> { // Stack IS-A list? ❌
5 public void push(int item) { add(item); }
6 public int popTop() { return remove(size() - 1); }
7 // Problem: exposes ALL ArrayList methods (add, remove, clear, etc.)
8}
9
10// ✓ Composition — correct model
11class Stack {
12 private final List<Integer> data = new ArrayList<>(); // Stack HAS-A list
13
14 public void push(int item) { data.add(item); }
15 public int pop() { return data.remove(data.size() - 1); }
16 public int peek() { return data.get(data.size() - 1); }
17 public boolean isEmpty() { return data.isEmpty(); }
18 // Only exposes Stack-specific API ✅
19}
20
21// Runtime-swappable behaviour via composition (Strategy Pattern)
22interface SortStrategy {
23 void sort(List<Integer> list);
24}
25
26class QuickSort implements SortStrategy {
27 public void sort(List<Integer> list) {
28 Collections.sort(list); // placeholder for quicksort logic
29 System.out.println("QuickSort used");
30 }
31}
32
33class MergeSort implements SortStrategy {
34 public void sort(List<Integer> list) {
35 Collections.sort(list); // placeholder for mergesort logic
36 System.out.println("MergeSort used");
37 }
38}
39
40class Sorter {
41 private SortStrategy strategy;
42
43 public Sorter(SortStrategy strategy) {
44 this.strategy = strategy;
45 }
46
47 public void setStrategy(SortStrategy strategy) {
48 this.strategy = strategy; // swap at runtime
49 }
50
51 public void sort(List<Integer> list) {
52 strategy.sort(list);
53 }
54}
55
56public class Main {
57 public static void main(String[] args) {
58 // Stack example
59 Stack stack = new Stack();
60 stack.push(10);
61 stack.push(20);
62 System.out.println(stack.pop());
63
64 // Strategy example
65 List<Integer> data = new ArrayList<>(Arrays.asList(5, 3, 8, 1));
66
67 Sorter sorter = new Sorter(new QuickSort());
68 sorter.sort(data);
69
70 sorter.setStrategy(new MergeSort()); // swap strategy at runtime
71 sorter.sort(data);
72 }
73}
74Deep inheritance hierarchies (more than 2–3 levels) are a code smell. When you find yourself adding empty method overrides to prevent inheritance side effects, that is a sign to refactor to composition.
25. What is cohesion and coupling in software design?
Cohesion and coupling are the two most fundamental measures of software design quality. The goal is always: high cohesion, low coupling.
Cohesion: How strongly the responsibilities within a single class or module are related to each other. High cohesion means the class does one thing well (SRP). Low cohesion means the class does many unrelated things (god class) — hard to understand, test, and reuse.
Coupling: The degree of dependence between different classes or modules. Low coupling means classes can be changed, tested, and reused independently. High coupling means changing one class requires cascading changes across many others — fragile and hard to maintain.
How to reduce coupling: Depend on interfaces/abstractions (DIP). Use dependency injection. Communicate via events/messages rather than direct method calls. Apply the Law of Demeter ('talk only to your immediate friends').
A well-designed system has components that are internally cohesive (each component does one thing well) and externally loosely coupled (components depend on each other only through well-defined interfaces).
26. Explain the Singleton pattern with a thread-safe implementation.
The Singleton pattern ensures a class has only one instance throughout the application's lifetime and provides a global access point to it. Use it for resources that are expensive to create, shared globally, and where exactly one instance is a hard requirement — such as a DB connection pool, logger, or config manager.
1class DatabasePool {
2
3 private static volatile DatabasePool instance; // volatile is critical
4 private int poolSize;
5
6 private DatabasePool() {
7 this.poolSize = 10;
8 System.out.println("[DB Pool] Initialised with 10 connections");
9 }
10
11 public static DatabasePool getInstance() {
12 if (instance == null) {
13 synchronized (DatabasePool.class) { // double-checked locking
14 if (instance == null) {
15 instance = new DatabasePool();
16 }
17 }
18 }
19 return instance;
20 }
21
22 public String getConnection() {
23 return "conn from pool (size=" + poolSize + ")";
24 }
25}
26
27public class Main {
28 public static void main(String[] args) {
29 DatabasePool poolA = DatabasePool.getInstance();
30 DatabasePool poolB = DatabasePool.getInstance();
31
32 System.out.println(poolA == poolB); // true
33 System.out.println(poolA.hashCode() == poolB.hashCode()); // true
34 System.out.println(poolA.getConnection());
35 }
36}
37
38// ⚠ Critique: Singleton introduces global state and makes unit testing hard.
39// Prefer dependency injection over Singleton where possible.
40// Only use Singleton when ONE instance is a strict requirement.
41Singleton is the most frequently misused pattern. It violates the Dependency Inversion Principle and makes mocking in tests difficult. Always ask: 'Could I achieve the same result with a single well-managed instance passed via dependency injection?' If yes, skip Singleton.
27. Explain the Factory pattern with a code example.
The Factory Method pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. It encapsulates the object creation logic, decoupling it from the client code that uses the objects.

1import java.util.*;
2
3// ── Abstract Processor ───────────────────────────────────────
4interface PaymentProcessor {
5 String process(double amount);
6}
7
8// ── Concrete Implementations ─────────────────────────────────
9class UPIProcessor implements PaymentProcessor {
10 public String process(double amount) {
11 return "₹" + amount + " paid via UPI";
12 }
13}
14
15class CardProcessor implements PaymentProcessor {
16 public String process(double amount) {
17 return "₹" + amount + " charged to card";
18 }
19}
20
21class WalletProcessor implements PaymentProcessor {
22 public String process(double amount) {
23 return "₹" + amount + " deducted from wallet";
24 }
25}
26
27// ── Factory ─────────────────────────────────────────────────
28class PaymentFactory {
29 private static final Map<String, Class<? extends PaymentProcessor>> registry = new HashMap<>();
30
31 static {
32 registry.put("upi", UPIProcessor.class);
33 registry.put("card", CardProcessor.class);
34 registry.put("wallet", WalletProcessor.class);
35 }
36
37 public static PaymentProcessor create(String method) {
38 Class<? extends PaymentProcessor> clazz = registry.get(method.toLowerCase());
39 if (clazz == null) {
40 throw new IllegalArgumentException("Unknown payment method: " + method);
41 }
42 try {
43 return clazz.getDeclaredConstructor().newInstance();
44 } catch (Exception e) {
45 throw new RuntimeException(e);
46 }
47 }
48
49 // Extension point — register new processors without modifying factory
50 public static void register(String method, Class<? extends PaymentProcessor> clazz) {
51 registry.put(method.toLowerCase(), clazz);
52 }
53}
54
55// ── New Payment Type (Runtime Extension) ─────────────────────
56class CryptoProcessor implements PaymentProcessor {
57 public String process(double amount) {
58 return "₹" + amount + " paid in crypto";
59 }
60}
61
62// ── Client Code ─────────────────────────────────────────────
63public class Main {
64 public static void main(String[] args) {
65 for (String method : Arrays.asList("upi", "card", "wallet")) {
66 PaymentProcessor processor = PaymentFactory.create(method);
67 System.out.println(processor.process(1000));
68 }
69
70 // Register new type at runtime
71 PaymentFactory.register("crypto", CryptoProcessor.class);
72 System.out.println(PaymentFactory.create("crypto").process(5000));
73 }
74}
75The registry-based factory (shown above) is the most extensible variant — new types can be registered without touching the factory code. This pattern is used extensively in Django's database backends, Python's logging handlers, and Spring's bean factory.
28. Explain the Observer pattern with a real-world example.
The Observer pattern defines a one-to-many dependency between objects.Observer is a behavioral design pattern that lets you define a subscription mechanism to notify multiple objects about any events that happen to the object they’re observing.
1import java.util.*;
2
3// ── Observer Interface ───────────────────────────────────────
4interface OrderObserver {
5 void onOrderPlaced(String orderId, double total);
6}
7
8// ── Concrete Observers ───────────────────────────────────────
9class EmailNotifier implements OrderObserver {
10 public void onOrderPlaced(String orderId, double total) {
11 System.out.println("[Email] Order " + orderId + " confirmed. Total ₹" + total);
12 }
13}
14
15class SMSNotifier implements OrderObserver {
16 public void onOrderPlaced(String orderId, double total) {
17 System.out.println("[SMS] Your order " + orderId + " is placed! Pay ₹" + total + ".");
18 }
19}
20
21class InventoryService implements OrderObserver {
22 public void onOrderPlaced(String orderId, double total) {
23 System.out.println("[Inventory] Reserving items for order " + orderId);
24 }
25}
26
27class AnalyticsService implements OrderObserver {
28 public void onOrderPlaced(String orderId, double total) {
29 System.out.println("[Analytics] Logging conversion event for order " + orderId + ", GMV ₹" + total);
30 }
31}
32
33// ── Subject (Publisher) ──────────────────────────────────────
34class OrderService {
35 private final List<OrderObserver> observers = new ArrayList<>();
36
37 public void subscribe(OrderObserver obs) {
38 observers.add(obs);
39 }
40
41 public void unsubscribe(OrderObserver obs) {
42 observers.remove(obs);
43 }
44
45 public void placeOrder(String orderId, double total) {
46 System.out.println("\nOrder " + orderId + " placed for ₹" + total);
47 for (OrderObserver obs : observers) {
48 obs.onOrderPlaced(orderId, total);
49 }
50 }
51}
52
53// ── Client Code ─────────────────────────────────────────────
54public class Main {
55 public static void main(String[] args) {
56 OrderService svc = new OrderService();
57
58 svc.subscribe(new EmailNotifier());
59 svc.subscribe(new SMSNotifier());
60 svc.subscribe(new InventoryService());
61 svc.subscribe(new AnalyticsService());
62
63 svc.placeOrder("ORD-001", 2450.00);
64 }
65}
66Observer decouples the OrderService from all downstream systems — adding a new observer (e.g., a fraud detection service) requires zero changes to OrderService. This is the principle behind domain events, webhook systems, and reactive frameworks.
29. Explain the Strategy pattern with a payment example.
Strategy is a behavioral design pattern that lets you define a family of algorithms, put each of them into a separate class, and make their objects interchangeable. It eliminates large if-else chains for different behaviours and lets the algorithm vary independently from the client that uses it.
1import java.util.*;
2
3// ── Strategy Interface ───────────────────────────────────────
4interface PaymentStrategy {
5 String pay(double amount);
6}
7
8// ── Concrete Strategies ──────────────────────────────────────
9class UPIPayment implements PaymentStrategy {
10 public String pay(double amount) {
11 return "₹" + amount + " paid via UPI";
12 }
13}
14
15class CardPayment implements PaymentStrategy {
16 public String pay(double amount) {
17 return "₹" + amount + " paid using Card";
18 }
19}
20
21class WalletPayment implements PaymentStrategy {
22 public String pay(double amount) {
23 return "₹" + amount + " paid from Wallet";
24 }
25}
26
27// ── Context ──────────────────────────────────────────────────
28class PaymentContext {
29 private PaymentStrategy strategy;
30
31 public PaymentContext(PaymentStrategy strategy) {
32 this.strategy = strategy;
33 }
34
35 public void setStrategy(PaymentStrategy strategy) {
36 this.strategy = strategy;
37 }
38
39 public String pay(double amount) {
40 return strategy.pay(amount);
41 }
42}
43
44// ── Client Code ─────────────────────────────────────────────
45public class Main {
46 public static void main(String[] args) {
47 PaymentContext ctx = new PaymentContext(new UPIPayment());
48 System.out.println(ctx.pay(1000));
49
50 ctx.setStrategy(new CardPayment());
51 System.out.println(ctx.pay(2000));
52
53 ctx.setStrategy(new WalletPayment());
54 System.out.println(ctx.pay(500));
55 }
56}
57Strategy is one of the most valuable patterns for interviews. It applies to pricing engines, sorting algorithms, compression codecs, routing algorithms, authentication methods, and any scenario where behaviour must be swappable at runtime.
30. Explain the Decorator pattern with a code example.
Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors. It is a flexible alternative to subclassing — you compose behaviour from small building blocks without modifying existing classes.
1import java.util.*;
2
3// ── Component Interface ──────────────────────────────────────
4interface Coffee {
5 double cost();
6 String description();
7}
8
9// ── Concrete Components ──────────────────────────────────────
10class Espresso implements Coffee {
11 public double cost() { return 60.0; }
12 public String description() { return "Espresso"; }
13}
14
15class FilterCoffee implements Coffee {
16 public double cost() { return 40.0; }
17 public String description() { return "Filter Coffee"; }
18}
19
20// ── Decorator Base Class ─────────────────────────────────────
21abstract class CoffeeDecorator implements Coffee {
22 protected Coffee coffee;
23
24 public CoffeeDecorator(Coffee coffee) {
25 this.coffee = coffee;
26 }
27
28 public double cost() { return coffee.cost(); }
29 public String description() { return coffee.description(); }
30}
31
32// ── Concrete Decorators ──────────────────────────────────────
33class Milk extends CoffeeDecorator {
34 public Milk(Coffee coffee) { super(coffee); }
35
36 public double cost() { return super.cost() + 20; }
37 public String description() { return super.description() + " + Milk"; }
38}
39
40class Sugar extends CoffeeDecorator {
41 public Sugar(Coffee coffee) { super(coffee); }
42
43 public double cost() { return super.cost() + 5; }
44 public String description() { return super.description() + " + Sugar"; }
45}
46
47class ExtraShot extends CoffeeDecorator {
48 public ExtraShot(Coffee coffee) { super(coffee); }
49
50 public double cost() { return super.cost() + 35; }
51 public String description() { return super.description() + " + Extra Shot"; }
52}
53
54class Vanilla extends CoffeeDecorator {
55 public Vanilla(Coffee coffee) { super(coffee); }
56
57 public double cost() { return super.cost() + 30; }
58 public String description() { return super.description() + " + Vanilla"; }
59}
60
61// ── Client Code ─────────────────────────────────────────────
62public class Main {
63 public static void main(String[] args) {
64 Coffee order1 = new Vanilla(new ExtraShot(new Milk(new Espresso())));
65 System.out.println(order1.description() + " → ₹" + order1.cost());
66
67 Coffee order2 = new Sugar(new Milk(new Sugar(new FilterCoffee())));
68 System.out.println(order2.description() + " → ₹" + order2.cost());
69 }
70}
71Real-world decorators: Python's @functools.lru_cache, Java's BufferedInputStream wrapping FileInputStream, HTTP middleware pipelines (auth → rate-limit → logging → handler). The key insight: decorators compose, while inheritance explodes into a combinatorial subclass hierarchy.
31. Explain the Command pattern with undo/redo support.
The Command pattern is a behavioral design pattern that turns a request into a stand-alone object that contains all information about the request. This transformation lets you pass requests as a method arguments, delay or queue a request’s execution, and support undoable operations.
1import java.util.*;
2
3interface Command { void execute(); void undo(); }
4
5class Light {
6 boolean on = false;
7 void turnOn() { on = true; }
8 void turnOff() { on = false; }
9}
10
11class LightOn implements Command {
12 private Light light;
13 LightOn(Light l) { this.light = l; }
14 public void execute() { light.turnOn(); }
15 public void undo() { light.turnOff(); }
16}
17
18class LightOff implements Command {
19 private Light light;
20 LightOff(Light l) { this.light = l; }
21 public void execute() { light.turnOff(); }
22 public void undo() { light.turnOn(); }
23}
24
25class Remote {
26 Deque<Command> undo = new ArrayDeque<>();
27 Deque<Command> redo = new ArrayDeque<>();
28
29 void press(Command c) { c.execute(); undo.push(c); redo.clear(); }
30 void undo() { if (!undo.isEmpty()) { Command c = undo.pop(); c.undo(); redo.push(c); } }
31 void redo() { if (!redo.isEmpty()) { Command c = redo.pop(); c.execute(); undo.push(c); } }
32}
33
34public class Main {
35 public static void main(String[] args) {
36 Light light = new Light();
37 Remote remote = new Remote();
38
39 remote.press(new LightOn(light));
40 System.out.println(light.on); // true
41
42 remote.undo();
43 System.out.println(light.on); // false
44
45 remote.redo();
46 System.out.println(light.on); // true
47 }
48}
49Command is used in GUI undo/redo stacks, database transaction logs, task queues (Celery tasks are Command objects), macro recording, and transactional outbox patterns. The history stack is simply a deque of Command objects — elegant and powerful.
32. Explain the State pattern and when to use it over if-else.
The State pattern is a behavioral design pattern that lets an object alter its behavior when its internal state changes. It appears as if the object changed its class. The object appears to change its class. Instead of a massive switch/if-else block checking the current state, each state is encapsulated in its own class with its own behaviour.
1import java.util.*;
2
3// ── State Interface ─────────────────────────────────────────
4interface TrafficLightState {
5 void handle(TrafficLight light);
6 String description();
7}
8
9// ── Concrete States ─────────────────────────────────────────
10class RedState implements TrafficLightState {
11 public void handle(TrafficLight light) {
12 System.out.println("RED → stop all vehicles");
13 light.setState(light.getGreenState());
14 }
15 public String description() { return "RED"; }
16}
17
18class GreenState implements TrafficLightState {
19 public void handle(TrafficLight light) {
20 System.out.println("GREEN → vehicles may proceed");
21 light.setState(light.getYellowState());
22 }
23 public String description() { return "GREEN"; }
24}
25
26class YellowState implements TrafficLightState {
27 public void handle(TrafficLight light) {
28 System.out.println("YELLOW → prepare to stop");
29 light.setState(light.getRedState());
30 }
31 public String description() { return "YELLOW"; }
32}
33
34// ── Context ─────────────────────────────────────────────────
35class TrafficLight {
36 private final TrafficLightState redState = new RedState();
37 private final TrafficLightState greenState = new GreenState();
38 private final TrafficLightState yellowState = new YellowState();
39
40 private TrafficLightState state = redState;
41
42 public void change() {
43 state.handle(this);
44 }
45
46 public String status() {
47 return state.description();
48 }
49
50 public void setState(TrafficLightState state) {
51 this.state = state;
52 }
53
54 public TrafficLightState getRedState() { return redState; }
55 public TrafficLightState getGreenState() { return greenState; }
56 public TrafficLightState getYellowState() { return yellowState; }
57}
58
59// ── Client Code ─────────────────────────────────────────────
60public class Main {
61 public static void main(String[] args) {
62 TrafficLight light = new TrafficLight();
63 System.out.println("Initial: " + light.status());
64
65 for (int i = 0; i < 6; i++) {
66 light.change();
67 System.out.println("Now: " + light.status());
68 }
69 }
70}
71Use State pattern when an object has complex, multi-step lifecycle transitions (Order, ATM machine, Vending machine, TCP connection, Elevator). Replace nested if-else / switch-case with State when adding a new state would require modifying many existing branches.
33. Explain the Builder pattern with a practical example.
The Builder pattern is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code. It is ideal when an object has many optional parameters — avoiding telescoping constructors (a constructor with 10+ parameters).
1import java.util.*;
2
3// ── Product ────────────────────────────────────────────────
4class Pizza {
5 private final String size;
6 private final String crust;
7 private final String sauce;
8 private final List<String> toppings;
9 private final boolean extraCheese;
10 private final boolean thinBase;
11 private final boolean wellDone;
12
13 private Pizza(Builder builder) {
14 this.size = builder.size;
15 this.crust = builder.crust;
16 this.sauce = builder.sauce;
17 this.toppings = builder.toppings;
18 this.extraCheese = builder.extraCheese;
19 this.thinBase = builder.thinBase;
20 this.wellDone = builder.wellDone;
21 }
22
23 public static class Builder {
24 private final String size;
25 private String crust = "regular";
26 private String sauce = "tomato";
27 private List<String> toppings = new ArrayList<>();
28 private boolean extraCheese = false;
29 private boolean thinBase = false;
30 private boolean wellDone = false;
31
32 public Builder(String size) {
33 this.size = size;
34 }
35
36 public Builder crust(String crust) {
37 this.crust = crust;
38 return this;
39 }
40
41 public Builder sauce(String sauce) {
42 this.sauce = sauce;
43 return this;
44 }
45
46 public Builder addTopping(String topping) {
47 this.toppings.add(topping);
48 return this;
49 }
50
51 public Builder extraCheese() {
52 this.extraCheese = true;
53 return this;
54 }
55
56 public Builder thinBase() {
57 this.thinBase = true;
58 return this;
59 }
60
61 public Builder wellDone() {
62 this.wellDone = true;
63 return this;
64 }
65
66 public Pizza build() {
67 return new Pizza(this);
68 }
69 }
70
71 @Override
72 public String toString() {
73 return "Pizza{" +
74 "size='" + size + '\'' +
75 ", crust='" + crust + '\'' +
76 ", sauce='" + sauce + '\'' +
77 ", toppings=" + toppings +
78 ", extraCheese=" + extraCheese +
79 ", thinBase=" + thinBase +
80 ", wellDone=" + wellDone +
81 '}';
82 }
83}
84
85// ── Client Code ─────────────────────────────────────────────
86public class Main {
87 public static void main(String[] args) {
88 Pizza pizza = new Pizza.Builder("large")
89 .crust("whole-wheat")
90 .sauce("pesto")
91 .addTopping("mushrooms")
92 .addTopping("olives")
93 .extraCheese()
94 .wellDone()
95 .build();
96
97 System.out.println(pizza);
98 }
99}
100Builder is heavily used in test fixture construction (test data builders), HTTP request builders, SQL query builders (SQLAlchemy), and configuration objects. It produces self-documenting, readable code compared to constructors with 8+ positional arguments.
Case Studies
1. Design a URL Shortener (like Bit.ly)
HLD (High-Level Design)
The system takes a long URL and returns a unique 6–8 character short code. On redirect, the short code is resolved to the original URL and the user is sent a 301/302 HTTP redirect. There are two core flows: write (shorten) and read (redirect). The read path is far more frequent (~100:1 ratio), so it must be heavily optimized with caching.
Key components: REST API server, a Base62 ID encoder, PostgreSQL for URL mappings, Redis for caching hot short codes, and a CDN for globally fast redirects. An analytics service captures click data (IP, timestamp, referrer) via a Kafka stream asynchronously so it never blocks the redirect.
Client → API Gateway → App Server → Redis (cache) → PostgreSQL (source of truth)
App Server → Kafka (click events) → Analytics Consumer → ClickHouse / BigQuery
301 = permanent redirect (browser caches, reduces server load). 302 = temporary (better for analytics, forces request every time)
LLD (Low-Level Design)
Use an auto-incremented integer ID from the DB and encode it with Base62 (a–z, A–Z, 0–9) to produce the short code. This avoids hash collisions and guarantees uniqueness. On GET, decode the short code back to the DB ID, look up Redis first, then fall back to PostgreSQL.
DB Schema
1// PostgreSQL schema (via JDBC)
2import java.sql.Connection;
3import java.sql.DriverManager;
4import java.sql.Statement;
5
6public class UrlSchema {
7 public static void main(String[] args) throws Exception {
8 Connection conn = DriverManager.getConnection(
9 "jdbc:postgresql://localhost/urlshortener", "user", "pass"
10 );
11 Statement stmt = conn.createStatement();
12
13 stmt.execute(
14 "CREATE TABLE IF NOT EXISTS urls (" +
15 " id BIGSERIAL PRIMARY KEY," +
16 " short_code VARCHAR(10) UNIQUE NOT NULL," +
17 " original_url TEXT NOT NULL," +
18 " user_id UUID," +
19 " expires_at TIMESTAMPTZ," +
20 " created_at TIMESTAMPTZ DEFAULT NOW()" +
21 ");"
22 );
23 stmt.execute(
24 "CREATE INDEX IF NOT EXISTS idx_short_code ON urls(short_code);"
25 );
26 // Redis cache usage (TTL = 24h)
27 // jedis.setex("url:" + shortCode, 86400, originalUrl);
28 stmt.close();
29 conn.close();
30 }
31}
32Base62 Encoding + Redirect Handler
1import redis.clients.jedis.Jedis;
2import java.sql.*;
3
4public class UrlShortener {
5 private static final String BASE62 =
6 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
7 private final Jedis jedis;
8 private final Connection db;
9
10 public UrlShortener(Jedis jedis, Connection db) {
11 this.jedis = jedis;
12 this.db = db;
13 }
14
15 public String encode(long id) {
16 StringBuilder sb = new StringBuilder();
17 while (id > 0) {
18 sb.insert(0, BASE62.charAt((int)(id % 62)));
19 id /= 62;
20 }
21 while (sb.length() < 6) sb.insert(0, '0'); // always 6 chars
22 return sb.toString();
23 }
24
25 public long decode(String shortCode) {
26 long result = 0;
27 for (char c : shortCode.toCharArray())
28 result = result * 62 + BASE62.indexOf(c);
29 return result;
30 }
31
32 public String redirect(String shortCode) throws Exception {
33 // 1. Check Redis cache first
34 String cached = jedis.get("url:" + shortCode);
35 if (cached != null) return cached;
36
37 // 2. Fallback to DB
38 PreparedStatement ps = db.prepareStatement(
39 "SELECT original_url FROM urls WHERE short_code = ?"
40 );
41 ps.setString(1, shortCode);
42 ResultSet rs = ps.executeQuery();
43 if (!rs.next()) throw new RuntimeException("URL not found");
44 String originalUrl = rs.getString("original_url");
45
46 // 3. Populate cache for next requests
47 jedis.setex("url:" + shortCode, 86400, originalUrl);
48 return originalUrl;
49 }
50}
51POST /shorten { url, customAlias?, expiresAt? } → returns { shortUrl }
GET /{code} → 302 redirect to original URL (async click event fired to Kafka)
Background cron: DELETE FROM urls WHERE expires_at < NOW() to clean up expired entries
2. Design a Rate Limiter
HLD
A rate limiter enforces a maximum number of requests a client can make in a time window (e.g., 100 req/min per user). It sits as middleware at the API Gateway level — before requests hit backend services. This prevents abuse, ensures fair usage, and protects against DDoS. It uses Redis as the shared counter store so that all app server instances see the same state.
Common algorithms: Fixed Window (simple but allows burst at edges), Sliding Window Log (accurate, high memory), Token Bucket (smooth bursts, used by AWS), Leaky Bucket (strict output rate). For most APIs, Sliding Window Counter (hybrid) is the sweet spot — accurate and memory efficient.
Client → Load Balancer → Rate Limiter Middleware → API Server
Redis stores per-user counters. Lua script makes check + increment atomic (no race conditions)
Returns HTTP 429 with Retry-After header when limit exceeded
LLD
Redis Lua Script (atomic check + increment)
1// Runs atomically inside Redis via Lua script — no race conditions
2import redis.clients.jedis.Jedis;
3import java.util.Arrays;
4import java.util.List;
5
6public class RateLimiter {
7 private static final String LUA_SCRIPT =
8 "local key = KEYS[1]\n" +
9 "local limit = tonumber(ARGV[1])\n" +
10 "local window = tonumber(ARGV[2])\n" +
11 "local current = redis.call('INCR', key)\n" +
12 "if current == 1 then\n" +
13 " redis.call('EXPIRE', key, window)\n" +
14 "end\n" +
15 "if current > limit then return 0 else return 1 end";
16
17 private final Jedis jedis;
18
19 public RateLimiter(Jedis jedis) { this.jedis = jedis; }
20
21 public boolean checkRateLimit(String userId, int limit, int window) {
22 long windowStart = (System.currentTimeMillis() / 1000 / window) * window;
23 String key = "rate:" + userId + ":" + windowStart;
24 List<String> keys = Arrays.asList(key);
25 List<String> args = Arrays.asList(String.valueOf(limit), String.valueOf(window));
26 Object result = jedis.eval(LUA_SCRIPT, keys, args);
27 return ((Long) result) == 1L; // true = allowed, false = rejected
28 }
29}
30Express Middleware
1// Spring Boot filter — rate limiter middleware
2import jakarta.servlet.*;
3import jakarta.servlet.http.*;
4import org.springframework.stereotype.Component;
5import redis.clients.jedis.Jedis;
6import java.io.IOException;
7import java.util.Arrays;
8
9@Component
10public class RateLimiterFilter implements Filter {
11 private static final int LIMIT = 100;
12 private final Jedis jedis = new Jedis("localhost", 6379);
13 private static final String LUA_SCRIPT =
14 "local c = redis.call('INCR', KEYS[1])\n" +
15 "if c == 1 then redis.call('EXPIRE', KEYS[1], ARGV[2]) end\n" +
16 "if c > tonumber(ARGV[1]) then return 0 else return 1 end";
17
18 @Override
19 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
20 throws IOException, ServletException {
21 HttpServletRequest httpReq = (HttpServletRequest) req;
22 HttpServletResponse httpRes = (HttpServletResponse) res;
23
24 String userId = httpReq.getRemoteAddr();
25 long windowStart = (System.currentTimeMillis() / 1000 / 60) * 60;
26 String key = "rate:" + userId + ":" + windowStart;
27
28 Object result = jedis.eval(
29 LUA_SCRIPT,
30 Arrays.asList(key),
31 Arrays.asList(String.valueOf(LIMIT), "60")
32 );
33
34 if (((Long) result) == 0L) {
35 httpRes.setHeader("Retry-After", "60");
36 httpRes.setHeader("X-RateLimit-Limit", String.valueOf(LIMIT));
37 httpRes.setStatus(429);
38 httpRes.getWriter().write("{\"error\": \"Too Many Requests\"}");
39 return;
40 }
41 chain.doFilter(req, res);
42 }
43}
443. Design Pastebin
HLD
Pastebin stores large text blobs (code, logs, notes) and returns a short shareable URL. The key design decision: store text content in object storage (S3) rather than the DB, since blobs can be MBs in size. Only lightweight metadata (ID, owner, expiry, visibility) is stored in PostgreSQL. A CDN sits in front of S3 so frequently accessed pastes are served from edge nodes at low latency.
Write: Client → API Server → save blob to S3 → save metadata to PostgreSQL → return short URL
Read: Client → API Server → check Redis (hot pastes) → fetch from S3 → stream to client
Background cron job hard-deletes expired pastes from both S3 and DB
LLD
1import java.sql.*;
2
3public class PastebinSchema {
4 public static void main(String[] args) throws Exception {
5 Connection conn = DriverManager.getConnection(
6 "jdbc:postgresql://localhost/pastebin", "user", "pass"
7 );
8 Statement stmt = conn.createStatement();
9
10 stmt.execute(
11 "CREATE TABLE IF NOT EXISTS pastes (" +
12 " id VARCHAR(10) PRIMARY KEY," +
13 " user_id UUID," +
14 " title VARCHAR(255)," +
15 " language VARCHAR(50)," +
16 " s3_key TEXT NOT NULL," +
17 " visibility VARCHAR(10) DEFAULT 'public'" +
18 " CHECK (visibility IN ('public','private','unlisted'))," +
19 " expires_at TIMESTAMPTZ," +
20 " created_at TIMESTAMPTZ DEFAULT NOW()" +
21 ");"
22 );
23 stmt.execute(
24 "CREATE INDEX IF NOT EXISTS idx_user_pastes " +
25 "ON pastes(user_id, created_at DESC);"
26 );
27 stmt.close();
28 conn.close();
29 }
30}
311import software.amazon.awssdk.services.s3.S3Client;
2import software.amazon.awssdk.services.s3.model.*;
3import software.amazon.awssdk.core.sync.RequestBody;
4import redis.clients.jedis.Jedis;
5import java.sql.*;
6import java.time.Instant;
7import java.util.*;
8
9public class PastebinService {
10 private final S3Client s3;
11 private final Jedis jedis;
12 private final Connection db;
13 private static final String BUCKET = "pastebin-blobs";
14
15 public PastebinService(S3Client s3, Jedis jedis, Connection db) {
16 this.s3 = s3; this.jedis = jedis; this.db = db;
17 }
18
19 // POST /paste
20 public String createPaste(String content, String title, String language,
21 Integer expiresIn, String visibility, String userId)
22 throws Exception {
23 String pasteId = randomBase62(8);
24 String s3Key = "pastes/" + pasteId + ".txt";
25
26 s3.putObject(
27 PutObjectRequest.builder().bucket(BUCKET).key(s3Key)
28 .contentType("text/plain").build(),
29 RequestBody.fromString(content)
30 );
31
32 Timestamp expiresAt = expiresIn != null
33 ? Timestamp.from(Instant.now().plusSeconds(expiresIn)) : null;
34 PreparedStatement ps = db.prepareStatement(
35 "INSERT INTO pastes (id,user_id,title,language,s3_key,visibility,expires_at)"
36 + " VALUES (?,?,?,?,?,?,?)"
37 );
38 ps.setString(1, pasteId); ps.setObject(2, userId != null ? UUID.fromString(userId) : null);
39 ps.setString(3, title); ps.setString(4, language);
40 ps.setString(5, s3Key); ps.setString(6, visibility);
41 ps.setTimestamp(7, expiresAt);
42 ps.executeUpdate();
43 return "https://paste.example.com/" + pasteId;
44 }
45
46 // GET /paste/:id
47 public Map<String, Object> getPaste(String pasteId) throws Exception {
48 String cached = jedis.get("paste:" + pasteId);
49 if (cached != null) return Map.of("cached", cached);
50
51 PreparedStatement ps = db.prepareStatement("SELECT * FROM pastes WHERE id = ?");
52 ps.setString(1, pasteId);
53 ResultSet rs = ps.executeQuery();
54 if (!rs.next()) throw new RuntimeException("Not found or expired");
55
56 String s3Key = rs.getString("s3_key");
57 String content = s3.getObjectAsBytes(
58 GetObjectRequest.builder().bucket(BUCKET).key(s3Key).build()
59 ).asUtf8String();
60
61 Map<String, Object> result = new HashMap<>();
62 result.put("id", pasteId); result.put("content", content);
63 jedis.setex("paste:" + pasteId, 3600, result.toString());
64 return result;
65 }
66
67 private String randomBase62(int len) {
68 String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
69 StringBuilder sb = new StringBuilder();
70 Random rnd = new Random();
71 for (int i = 0; i < len; i++) sb.append(chars.charAt(rnd.nextInt(chars.length())));
72 return sb.toString();
73 }
74}
754. Design a Twitter/X News Feed
HLD
When a user posts a tweet it needs to appear in all their followers' feeds. Two strategies: Fanout on Write (push model) — immediately write the tweet ID to each follower's Redis feed list when posted. Fanout on Read (pull model) — merge followee timelines on request. Push is fast to read but expensive for high-follower accounts. Twitter uses a hybrid: push for normal users, pull for celebrities (>1M followers).
Tweet Service writes tweet to Tweet DB → publishes event to Kafka topic 'new-tweet'
Fanout Worker consumes Kafka, fetches follower list, writes tweet_id to Redis Sorted Set per follower
GET /feed → ZREVRANGE feed:{userId} → batch-fetch tweet details from cache → hydrate with user info
Celebrity tweets (>1M followers) are fetched on read and merged with the pre-built feed
LLD
1import java.sql.*;
2
3public class TwitterSchema {
4 public static void main(String[] args) throws Exception {
5 Connection conn = DriverManager.getConnection(
6 "jdbc:postgresql://localhost/twitter", "user", "pass"
7 );
8 Statement stmt = conn.createStatement();
9 stmt.execute(
10 "CREATE TABLE IF NOT EXISTS follows (" +
11 " follower_id UUID," +
12 " followee_id UUID," +
13 " created_at TIMESTAMPTZ DEFAULT NOW()," +
14 " PRIMARY KEY (follower_id, followee_id)" +
15 ");"
16 );
17 stmt.execute(
18 "CREATE INDEX IF NOT EXISTS idx_followee ON follows(followee_id);"
19 );
20 stmt.close();
21 conn.close();
22 }
23}
241import redis.clients.jedis.*;
2import java.util.*;
3
4public class TwitterFanout {
5 private static final long CELEBRITY_THRESHOLD = 1_000_000L;
6 private static final int PAGE_SIZE = 20;
7 private final Jedis jedis;
8
9 public TwitterFanout(Jedis jedis) { this.jedis = jedis; }
10
11 public void fanoutTweet(Tweet tweet, long followerCount, List<String> followers) {
12 if (followerCount > CELEBRITY_THRESHOLD) return;
13
14 Pipeline pipe = jedis.pipelined();
15 for (String followerId : followers) {
16 String feedKey = "feed:" + followerId;
17 pipe.zadd(feedKey, (double) tweet.timestamp, tweet.tweetId);
18 pipe.zremrangeByRank(feedKey, 0, -801); // cap at 800
19 }
20 pipe.sync();
21 }
22
23 public List<String> getFeed(String userId, int page) {
24 long start = (long) page * PAGE_SIZE;
25 List<String> tweetIds = new ArrayList<>(
26 jedis.zrevrange("feed:" + userId, start, start + PAGE_SIZE - 1)
27 );
28 tweetIds.addAll(fetchCelebrityTweets(userId));
29 tweetIds.sort(Comparator.reverseOrder());
30 return tweetIds.subList(0, Math.min(PAGE_SIZE, tweetIds.size()));
31 }
32
33 private List<String> fetchCelebrityTweets(String userId) { return List.of(); }
34
35 static class Tweet {
36 String tweetId, userId, content;
37 long timestamp;
38 }
39}
405. Design a Distributed Key-Value Store
HLD
A distributed KV store (like DynamoDB or Cassandra) shards data across multiple nodes using consistent hashing so that adding/removing nodes only remaps a fraction of keys. Each key is replicated to N successor nodes (typically N=3) for fault tolerance. Reads and writes require a quorum: R + W > N ensures strong consistency. If R=1, W=3 → strong write consistency. If R=2, W=2 → balanced read/write.
Consistent hashing ring: each node owns a key range. Virtual nodes (vnodes) prevent hotspots.
Coordinator node routes requests — does not need to be the data owner
Gossip protocol: nodes periodically share membership state — no SPOF for failure detection
Hinted handoff: writes for a failed node are temporarily held by coordinator and replayed on recovery
LLD
1import java.security.MessageDigest;
2import java.math.BigInteger;
3import java.util.*;
4
5public class ConsistentHashRing {
6 private final int vnodes;
7 private final TreeMap<BigInteger, String> ring;
8 private final Set<String> nodes;
9
10 public ConsistentHashRing(int vnodes) {
11 this.vnodes = vnodes;
12 this.ring = new TreeMap<>();
13 this.nodes = new HashSet<>();
14 }
15
16 private BigInteger hash(String key) {
17 try {
18 MessageDigest md = MessageDigest.getInstance("MD5");
19 return new BigInteger(1, md.digest(key.getBytes()));
20 } catch (Exception e) { throw new RuntimeException(e); }
21 }
22
23 public void addNode(String node) {
24 nodes.add(node);
25 for (int i = 0; i < vnodes; i++)
26 ring.put(hash(node + ":" + i), node);
27 }
28
29 public void removeNode(String node) {
30 nodes.remove(node);
31 ring.entrySet().removeIf(e -> e.getValue().equals(node));
32 }
33
34 public String getNode(String key) {
35 BigInteger h = hash(key);
36 SortedMap<BigInteger, String> tail = ring.tailMap(h);
37 BigInteger nodeHash = tail.isEmpty() ? ring.firstKey() : tail.firstKey();
38 return ring.get(nodeHash);
39 }
40
41 public List<String> getReplicaNodes(String key, int n) {
42 String primary = getNode(key);
43 Set<String> seen = new HashSet<>(Set.of(primary));
44 List<String> replicas = new ArrayList<>(List.of(primary));
45 for (String node : ring.values()) {
46 if (replicas.size() >= n) break;
47 if (!seen.contains(node)) { seen.add(node); replicas.add(node); }
48 }
49 return replicas;
50 }
51}
52Storage engine: LSM-Tree (Write → MemTable → SSTable on disk). Fast writes; compaction handles read performance
Vector clocks detect conflicting writes during network partition. Last-write-wins or app-level merge resolves conflicts
6. Design a Search Autocomplete System
HLD
Autocomplete must respond in under 100ms. A Trie stores popular query prefixes with top-K suggestions cached at each node. A data pipeline (Kafka → Spark batch job) aggregates search logs daily and rebuilds the Trie. The Trie is serialized into Redis Sorted Sets for fast in-memory lookup. The client debounces keystrokes to reduce API call frequency.
Client (300ms debounce) → CDN (caches single-char prefixes) → API Server → Redis Sorted Sets
Kafka collects search events → Spark aggregates top queries per prefix → updates Redis nightly
Filter service strips banned/adult suggestions before returning to client
LLD
1import redis.clients.jedis.*;
2import java.util.*;
3
4public class SearchAutocomplete {
5 private final Jedis jedis;
6
7 public SearchAutocomplete(Jedis jedis) { this.jedis = jedis; }
8
9 // Update suggestions (run by Spark output loader nightly)
10 public void updateSuggestion(String query, double frequency) {
11 Pipeline pipe = jedis.pipelined();
12 for (int i = 1; i <= query.length(); i++) {
13 String prefix = query.substring(0, i);
14 String key = "autocomplete:" + prefix;
15 pipe.zadd(key, frequency, query);
16 pipe.zremrangeByRank(key, 0, -6); // top 5 only
17 }
18 pipe.sync();
19 }
20
21 // GET /autocomplete?q=iphon
22 public List<String> getSuggestions(String prefix) {
23 String key = "autocomplete:" + prefix.toLowerCase();
24 return new ArrayList<>(
25 jedis.zrevrangeByScore(key, Double.MAX_VALUE, 0, 0, 5)
26 );
27 }
28
29 // Example Redis state after indexing:
30 // autocomplete:ip → { 'iphone 15': 982000, 'ipad': 540000 }
31 // autocomplete:iph → { 'iphone 15': 982000, 'iphone cases': 430000 }
32}
337. Design a File Storage System (like Google Drive)
HLD
Files are split into 4 MB chunks on the client before upload. Each chunk is identified by a SHA-256 checksum. If the checksum already exists in the DB, the chunk is skipped (deduplication). Only changed chunks are uploaded on edits (delta sync). Chunks are stored in object storage (S3); metadata (file tree, version history, chunk pointers) lives in PostgreSQL. A real-time sync service pushes change events to connected clients via WebSocket.
Client Sync Agent hashes each chunk → asks Metadata Service which chunks are missing → uploads only those
S3 pre-signed URLs allow the client to upload chunks directly — bypasses app server for large files
Notification Service (WebSocket) pushes file change events to all of the user's connected devices
LLD
1import java.sql.*;
2
3public class FileStorageSchema {
4 public static void main(String[] args) throws Exception {
5 Connection conn = DriverManager.getConnection(
6 "jdbc:postgresql://localhost/gdrive", "user", "pass"
7 );
8 Statement stmt = conn.createStatement();
9 stmt.execute(
10 "CREATE TABLE IF NOT EXISTS files (" +
11 " file_id UUID PRIMARY KEY, user_id UUID NOT NULL," +
12 " name TEXT, parent_id UUID REFERENCES files(file_id)," +
13 " created_at TIMESTAMPTZ DEFAULT NOW());"
14 );
15 stmt.execute(
16 "CREATE TABLE IF NOT EXISTS chunks (" +
17 " checksum CHAR(64) PRIMARY KEY," +
18 " s3_key TEXT NOT NULL, size_bytes INT);"
19 );
20 stmt.execute(
21 "CREATE TABLE IF NOT EXISTS file_versions (" +
22 " version_id UUID PRIMARY KEY," +
23 " file_id UUID REFERENCES files(file_id)," +
24 " size_bytes BIGINT, created_at TIMESTAMPTZ DEFAULT NOW());"
25 );
26 stmt.execute(
27 "CREATE TABLE IF NOT EXISTS version_chunks (" +
28 " version_id UUID REFERENCES file_versions(version_id)," +
29 " chunk_index INT," +
30 " checksum CHAR(64) REFERENCES chunks(checksum)," +
31 " PRIMARY KEY (version_id, chunk_index));"
32 );
33 stmt.close(); conn.close();
34 }
35}
361import java.io.*;
2import java.net.URI;
3import java.net.http.*;
4import java.nio.file.*;
5import java.security.*;
6import java.util.*;
7import com.fasterxml.jackson.databind.ObjectMapper;
8
9public class ChunkedUploader {
10 private static final int CHUNK_SIZE = 4 * 1024 * 1024;
11 private final HttpClient http = HttpClient.newHttpClient();
12 private final ObjectMapper mapper = new ObjectMapper();
13 private final String apiBase, authToken;
14
15 public ChunkedUploader(String apiBase, String authToken) {
16 this.apiBase = apiBase; this.authToken = authToken;
17 }
18
19 public void uploadFile(String filePath) throws Exception {
20 byte[] fileBytes = Files.readAllBytes(Path.of(filePath));
21 List<Map<String, Object>> chunks = new ArrayList<>();
22
23 // 1. Hash each chunk locally
24 for (int offset = 0; offset < fileBytes.length; offset += CHUNK_SIZE) {
25 byte[] blob = Arrays.copyOfRange(fileBytes, offset,
26 Math.min(offset + CHUNK_SIZE, fileBytes.length));
27 chunks.add(Map.of("checksum", sha256Hex(blob), "data", blob));
28 }
29
30 // 2. Ask server which chunks it does NOT have yet
31 List<String> checksums = chunks.stream()
32 .map(c -> (String) c.get("checksum")).toList();
33 String checkResp = post(apiBase + "/files/check-chunks",
34 mapper.writeValueAsString(Map.of("checksums", checksums)));
35 Set<String> missing = new HashSet<>(
36 mapper.readValue(mapper.readTree(checkResp).get("missing").toString(), List.class));
37
38 // 3. Upload only missing chunks
39 for (Map<String, Object> chunk : chunks) {
40 String cs = (String) chunk.get("checksum");
41 if (!missing.contains(cs)) continue;
42 String urlResp = post(apiBase + "/files/chunk-url",
43 mapper.writeValueAsString(Map.of("checksum", cs)));
44 String uploadUrl = mapper.readTree(urlResp).get("uploadUrl").asText();
45 put(uploadUrl, (byte[]) chunk.get("data"));
46 }
47
48 // 4. Finalize
49 post(apiBase + "/files/finalize",
50 mapper.writeValueAsString(Map.of("fileId",
51 Path.of(filePath).getFileName().toString(), "chunks", checksums)));
52 }
53
54 private String sha256Hex(byte[] data) throws Exception {
55 byte[] hash = MessageDigest.getInstance("SHA-256").digest(data);
56 StringBuilder sb = new StringBuilder();
57 for (byte b : hash) sb.append(String.format("%02x", b));
58 return sb.toString();
59 }
60
61 private String post(String url, String body) throws Exception {
62 return http.send(HttpRequest.newBuilder(URI.create(url))
63 .header("Content-Type", "application/json")
64 .header("Authorization", "Bearer " + authToken)
65 .POST(HttpRequest.BodyPublishers.ofString(body)).build(),
66 HttpResponse.BodyHandlers.ofString()).body();
67 }
68
69 private void put(String url, byte[] body) throws Exception {
70 http.send(HttpRequest.newBuilder(URI.create(url))
71 .PUT(HttpRequest.BodyPublishers.ofByteArray(body)).build(),
72 HttpResponse.BodyHandlers.discarding());
73 }
74}
758. Design a Real-Time Chat System (like WhatsApp)
HLD
Each client holds a persistent WebSocket connection to a Chat Server. When Alice sends a message to Bob, it is written to Kafka (durable log). A delivery consumer reads from Kafka and looks up which Chat Server Bob is connected to (via Redis presence), then pushes the message to Bob's WebSocket. If Bob is offline, the message is queued and a push notification is sent via FCM/APNs. Messages are persisted to Cassandra, chosen for its high write throughput and time-ordered queries per chat room.
WebSocket Servers are stateful (hold live connections). Session → server mapping stored in Redis: session:{userId} → serverId
Kafka decouples message send from delivery — protects against chat server failures
Cassandra partition key = room_id, clustering key = timestamp DESC — efficient last-N messages query
Media (images, audio): client uploads to S3 directly, sends URL in message body. CDN delivers media.
LLD
1import com.datastax.oss.driver.api.core.CqlSession;
2import org.apache.kafka.clients.producer.*;
3import redis.clients.jedis.*;
4import jakarta.websocket.*;
5import jakarta.websocket.server.ServerEndpoint;
6import com.fasterxml.jackson.databind.ObjectMapper;
7import java.util.*;
8
9@ServerEndpoint("/ws/chat")
10public class ChatWebSocketHandler {
11 private final CqlSession cassandra;
12 private final KafkaProducer<String, String> kafka;
13 private final Jedis jedis;
14 private final ObjectMapper mapper = new ObjectMapper();
15
16 public ChatWebSocketHandler(CqlSession cassandra,
17 KafkaProducer<String, String> kafka,
18 Jedis jedis) {
19 this.cassandra = cassandra; this.kafka = kafka; this.jedis = jedis;
20 }
21
22 @OnMessage
23 public void onMessage(Session session, String raw) throws Exception {
24 Map<String, Object> data = mapper.readValue(raw, Map.class);
25 String msgId = (String) data.get("msgId");
26 String roomId = (String) data.get("roomId");
27 String senderId = (String) data.get("senderId");
28 String content = (String) data.get("content");
29 String type = (String) data.get("type");
30 long ts = ((Number) data.get("timestamp")).longValue();
31
32 // 1. Persist to Cassandra (IF NOT EXISTS)
33 cassandra.execute(
34 "INSERT INTO messages (room_id,msg_id,sender_id,content,type,ts) " +
35 "VALUES (?,?,?,?,?,?) IF NOT EXISTS",
36 roomId, msgId, senderId, content, type, ts
37 );
38
39 // 2. Publish to Kafka for fan-out
40 kafka.send(new ProducerRecord<>("chat." + roomId,
41 mapper.writeValueAsString(data)));
42
43 // 3. ACK back to sender
44 session.getBasicRemote().sendText(
45 mapper.writeValueAsString(Map.of(
46 "type", "ack", "msgId", msgId, "status", "sent"
47 ))
48 );
49 }
50
51 public void deliverToMembers(List<String> members, Map<String, Object> msg,
52 String senderId) throws Exception {
53 for (String userId : members) {
54 if (userId.equals(senderId)) continue;
55 String serverId = jedis.get("session:" + userId);
56 if (serverId != null)
57 jedis.publish("server:" + serverId, mapper.writeValueAsString(msg));
58 else
59 sendPushNotification(userId, msg);
60 }
61 }
62
63 private void sendPushNotification(String userId, Map<String, Object> msg) {} // stub
64}
659. Design a Notification System
HLD
A notification system must support multiple channels: push (FCM for Android, APNs for iOS), email (SendGrid), and SMS (Twilio). The system decouples producers (other services that trigger notifications) from consumers (channel workers) using Kafka. This allows each channel to scale independently and retry failures without blocking the producer. A User Preference Service controls opt-ins, quiet hours, and per-channel frequency caps.
Event Producer → Notification Service → Kafka (topic per channel) → Channel Workers → 3rd-party providers
Deduplication: Redis SET with notif_id + TTL prevents duplicate sends on Kafka retry
Retry with exponential backoff for failed provider calls. Permanent failures go to a dead-letter queue
LLD
1import redis.clients.jedis.Jedis;
2import redis.clients.jedis.params.SetParams;
3import java.time.ZonedDateTime;
4import java.util.*;
5
6public class NotificationWorker {
7 private final Jedis jedis;
8
9 public NotificationWorker(Jedis jedis) { this.jedis = jedis; }
10
11 public void processPushNotification(NotificationEvent event) {
12 // 1. Deduplication
13 String isNew = jedis.set("notif:sent:" + event.notifId, "1",
14 new SetParams().nx().ex(86400));
15 if (isNew == null) return;
16
17 // 2. Check preferences and quiet hours
18 UserPrefs prefs = getUserPrefs(event.userId);
19 if (!prefs.pushEnabled || isQuietHour(prefs.quietStart, prefs.quietEnd)) return;
20
21 // 3. Send to each registered device
22 for (DeviceToken device : getDeviceTokens(event.userId)) {
23 try {
24 NotificationProvider provider =
25 device.platform.equals("android") ? new FcmProvider() : new ApnsProvider();
26 provider.send(device.token, event.title, event.body, event.data);
27 logNotification(event.notifId, event.userId, "push", "delivered");
28 } catch (Exception err) {
29 handleFailedDelivery(event, err);
30 }
31 }
32 }
33
34 private boolean isQuietHour(int start, int end) {
35 int hour = ZonedDateTime.now().getHour();
36 return hour >= start || hour < end;
37 }
38
39 private UserPrefs getUserPrefs(String uid) { return new UserPrefs(true, 23, 7); }
40 private List<DeviceToken> getDeviceTokens(String uid) { return List.of(); }
41 private void logNotification(String id, String uid, String ch, String s) {}
42 private void handleFailedDelivery(NotificationEvent e, Exception err) {}
43
44 record UserPrefs(boolean pushEnabled, int quietStart, int quietEnd) {}
45 record DeviceToken(String platform, String token) {}
46 interface NotificationProvider { void send(String t, String ti, String b, Map<String,String> d); }
47 static class FcmProvider implements NotificationProvider {
48 public void send(String t, String ti, String b, Map<String,String> d) {}
49 }
50 static class ApnsProvider implements NotificationProvider {
51 public void send(String t, String ti, String b, Map<String,String> d) {}
52 }
53 static class NotificationEvent {
54 String notifId, userId, type, title, body;
55 List<String> channels;
56 Map<String,String> data;
57 }
58}
5910. Design a Video Streaming Platform (like YouTube)
HLD
Two separate flows: upload and stream. On upload, the raw video is pushed to object storage, which triggers an async transcoding pipeline that produces multiple resolution variants (360p, 720p, 1080p) and splits each into 6-second HLS segments. On playback, the browser fetches an .m3u8 manifest and pulls segments from the CDN. Adaptive Bitrate Streaming (ABR) automatically switches quality based on the viewer's bandwidth.
Upload: Client → Upload Service (multipart) → S3 (raw) → SQS trigger → Transcoder Workers → S3 (HLS segments)
Stream: Player fetches master.m3u8 manifest → pulls .ts segments from CDN edge → buffers and plays
View count: Kafka stream → Spark/Flink aggregates per minute → writes to DB; counters cached in Redis
LLD
1import software.amazon.awssdk.services.s3.S3Client;
2import software.amazon.awssdk.services.s3.model.*;
3import software.amazon.awssdk.core.sync.RequestBody;
4import java.sql.*;
5import java.util.List;
6
7public class VideoTranscoder {
8 private final S3Client s3;
9 private final Connection db;
10 private static final String BUCKET = "video-bucket";
11
12 record Resolution(String name, int width, int height, String bitrate) {}
13
14 private static final List<Resolution> RESOLUTIONS = List.of(
15 new Resolution("360p", 640, 360, "800k"),
16 new Resolution("720p", 1280, 720, "2500k"),
17 new Resolution("1080p", 1920, 1080, "5000k")
18 );
19
20 public VideoTranscoder(S3Client s3, Connection db) {
21 this.s3 = s3; this.db = db;
22 }
23
24 public void transcodeVideo(String videoId, String rawS3Key) throws Exception {
25 PreparedStatement upd = db.prepareStatement(
26 "UPDATE videos SET status=? WHERE video_id=?");
27 upd.setString(1, "processing"); upd.setString(2, videoId);
28 upd.executeUpdate();
29
30 StringBuilder master = new StringBuilder("#EXTM3U\n");
31
32 for (Resolution res : RESOLUTIONS) {
33 String outputPrefix = "videos/" + videoId + "/" + res.name();
34
35 // Run FFmpeg: transcode + split into 6-second HLS segments
36 new ProcessBuilder(
37 "ffmpeg", "-i", "s3://" + rawS3Key,
38 "-vf", "scale=" + res.width() + ":" + res.height(),
39 "-b:v", res.bitrate(),
40 "-hls_time", "6",
41 "-hls_segment_filename", outputPrefix + "/%03d.ts",
42 outputPrefix + "/index.m3u8"
43 ).inheritIO().start().waitFor();
44
45 master.append("#EXT-X-STREAM-INF:BANDWIDTH=").append(res.bitrate())
46 .append(",RESOLUTION=").append(res.width()).append("x").append(res.height())
47 .append("\n").append("videos/").append(videoId).append("/")
48 .append(res.name()).append("/index.m3u8\n");
49 }
50
51 s3.putObject(
52 PutObjectRequest.builder().bucket(BUCKET)
53 .key("videos/" + videoId + "/master.m3u8")
54 .contentType("application/vnd.apple.mpegurl").build(),
55 RequestBody.fromString(master.toString())
56 );
57
58 String manifestUrl =
59 "https://cdn.example.com/videos/" + videoId + "/master.m3u8";
60 PreparedStatement ps = db.prepareStatement(
61 "UPDATE videos SET status=?, manifest_url=? WHERE video_id=?");
62 ps.setString(1, "ready"); ps.setString(2, manifestUrl); ps.setString(3, videoId);
63 ps.executeUpdate();
64 }
65}
6611. Design a Web Crawler
HLD
A web crawler starts with seed URLs, fetches HTML, extracts links, and schedules new URLs — forming a cycle. It must handle: deduplication (billions of URLs), politeness (not hammering a server), robots.txt compliance, and distributed scale. A URL Frontier (priority queue of queues, one per domain) manages crawl order. A Bloom filter provides probabilistic O(1) memory-efficient deduplication.
URL Frontier → Fetcher → HTML Parser → Link Extractor → Bloom Filter check → URL Frontier (loop)
Per-domain queue enforces ≥1s politeness delay between requests to same host
Content stored in HDFS/S3. Simhash detects near-duplicate pages before storage to save space
LLD
1import com.google.common.hash.BloomFilter;
2import com.google.common.hash.Funnels;
3import org.jsoup.Jsoup;
4import java.net.URI;
5import java.nio.charset.StandardCharsets;
6import java.util.*;
7import java.util.concurrent.ConcurrentHashMap;
8
9public class WebCrawler {
10 private static final long POLITENESS_MS = 1000L;
11
12 private final BloomFilter<String> bloomFilter =
13 BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8),
14 10_000_000, 0.01);
15 private final Map<String, Queue<String>> domainQueues = new ConcurrentHashMap<>();
16 private final Map<String, Long> domainLastFetch = new ConcurrentHashMap<>();
17
18 public void crawl(List<String> seedUrls) throws Exception {
19 for (String url : seedUrls) enqueue(url);
20
21 while (true) {
22 String url = dequeue();
23 if (url == null) { Thread.sleep(100); continue; }
24 try {
25 String html = fetch(url);
26 store(url, html);
27 for (String link : extractLinks(html, url)) {
28 if (!bloomFilter.mightContain(link)) {
29 bloomFilter.put(link);
30 enqueue(link);
31 }
32 }
33 } catch (Exception e) {
34 System.err.println("Error crawling " + url + ": " + e.getMessage());
35 }
36 }
37 }
38
39 private void enqueue(String url) {
40 try {
41 String domain = new URI(url).getHost();
42 domainQueues.computeIfAbsent(domain, k -> new LinkedList<>()).add(url);
43 } catch (Exception ignored) {}
44 }
45
46 private String dequeue() {
47 long now = System.currentTimeMillis();
48 for (Map.Entry<String, Queue<String>> entry : domainQueues.entrySet()) {
49 if (entry.getValue().isEmpty()) continue;
50 long last = domainLastFetch.getOrDefault(entry.getKey(), 0L);
51 if (now - last < POLITENESS_MS) continue;
52 domainLastFetch.put(entry.getKey(), now);
53 return entry.getValue().poll();
54 }
55 return null;
56 }
57
58 private String fetch(String url) throws Exception {
59 return Jsoup.connect(url).timeout(5000).get().html();
60 }
61
62 private List<String> extractLinks(String html, String baseUrl) {
63 List<String> links = new ArrayList<>();
64 Jsoup.parse(html, baseUrl).select("a[href]")
65 .forEach(el -> links.add(el.absUrl("href")));
66 return links;
67 }
68
69 private void store(String url, String html) {} // stub: write to S3/HDFS
70}
7112. Design a Distributed Cache (like Redis)
HLD
A distributed cache stores hot data in RAM across multiple nodes to reduce DB load and response latency. Data is sharded across cache nodes using consistent hashing. Each shard has a primary + read replicas. Clients use a smart library (or a proxy like Twemproxy) to route requests to the correct shard. The cache-aside pattern is most common: app reads cache first, populates on miss, invalidates on write.
Cache hit path: App → Redis shard (sub-millisecond). Cache miss: App → DB → populate Redis → return
Eviction policy: LRU (default) or LFU (frequency-aware). Memory limit enforced with maxmemory config
Persistence: RDB (snapshot every N seconds) or AOF (log every write). AOF is safer, RDB is faster to restore
LLD — LRU Cache implementation
1// LRU Cache — O(1) get/put using HashMap + Doubly Linked List
2import java.util.HashMap;
3
4public class LRUCache<K, V> {
5 private static class Node<K, V> {
6 K key; V val;
7 Node<K, V> prev, next;
8 Node(K key, V val) { this.key = key; this.val = val; }
9 }
10
11 private final int capacity;
12 private final HashMap<K, Node<K, V>> map = new HashMap<>();
13 private final Node<K, V> head; // MRU sentinel
14 private final Node<K, V> tail; // LRU sentinel
15
16 @SuppressWarnings("unchecked")
17 public LRUCache(int capacity) {
18 this.capacity = capacity;
19 head = new Node<>((K)(Object)null, (V)(Object)null);
20 tail = new Node<>((K)(Object)null, (V)(Object)null);
21 head.next = tail; tail.prev = head;
22 }
23
24 public V get(K key) {
25 Node<K, V> node = map.get(key);
26 if (node == null) return null;
27 moveToFront(node);
28 return node.val;
29 }
30
31 public void put(K key, V val) {
32 if (map.containsKey(key)) {
33 Node<K, V> node = map.get(key);
34 node.val = val; moveToFront(node);
35 } else {
36 if (map.size() >= capacity) {
37 Node<K, V> lru = tail.prev;
38 removeNode(lru); map.remove(lru.key);
39 }
40 Node<K, V> node = new Node<>(key, val);
41 addToFront(node); map.put(key, node);
42 }
43 }
44
45 private void moveToFront(Node<K, V> n) { removeNode(n); addToFront(n); }
46 private void addToFront(Node<K, V> n) {
47 n.next = head.next; n.prev = head;
48 head.next.prev = n; head.next = n;
49 }
50 private void removeNode(Node<K, V> n) {
51 n.prev.next = n.next; n.next.prev = n.prev;
52 }
53
54 public static void main(String[] args) {
55 LRUCache<String, Integer> cache = new LRUCache<>(3);
56 cache.put("a", 1); cache.put("b", 2); cache.put("c", 3);
57 System.out.println(cache.get("a")); // 1 — "a" is now MRU
58 cache.put("d", 4); // evicts "b" (LRU)
59 System.out.println(cache.get("b")); // null
60 }
61}
62Thundering herd: use a mutex/lock per key on cache miss — only 1 request populates from DB, rest wait on the lock
Cache stampede on startup: warm cache gradually with a background pre-warming job using lazy loading
13. Design a Ride-Sharing App (like Uber)
HLD
The hardest challenge is real-time geolocation matching at scale. Drivers broadcast their GPS position every 4 seconds. The Location Service stores live positions in Redis GEO (a geospatial sorted set). When a rider requests a trip, the Matching Service runs a GEORADIUS query to find nearby available drivers, scores them by ETA (via Maps API), and sends the top match a WebSocket/push offer. A state machine tracks the full trip lifecycle. Surge pricing is a separate service that reads supply/demand data from Kafka streams and adjusts the multiplier per geo-cell every 30 seconds.
Driver App → Location Service → GEOADD to Redis GEO every 4s
Rider App → Trip Request Service → Matching Service → GEORADIUS → ETA ranking → WebSocket offer to best driver
Trip DB: PostgreSQL (trip records, payments). Location history: Cassandra (append-only GPS logs for route replay)
Surge: Kafka stream of ride requests per H3 geo-cell → Flink sliding window → multiplier updated every 30s
LLD
1import redis.clients.jedis.*;
2import redis.clients.jedis.args.GeoUnit;
3import redis.clients.jedis.params.GeoRadiusParam;
4import java.sql.*;
5import java.util.*;
6
7public class RideSharingService {
8 private final Jedis jedis;
9 private final Connection db;
10
11 public RideSharingService(Jedis jedis, Connection db) {
12 this.jedis = jedis; this.db = db;
13 }
14
15 // Driver location update — called every 4 seconds
16 public void updateDriverLocation(String driverId, double lat, double lng) {
17 jedis.geoadd("drivers:available", lng, lat, driverId);
18 }
19
20 // Find nearest available drivers within radius
21 public List<GeoRadiusResponse> findNearbyDrivers(
22 double riderLat, double riderLng, double radiusKm) {
23 return jedis.georadius(
24 "drivers:available", riderLng, riderLat, radiusKm, GeoUnit.KM,
25 GeoRadiusParam.geoRadiusParam()
26 .withCoord().withDist().sortAscending().count(10)
27 );
28 }
29
30 // Trip state machine
31 public enum TripStatus {
32 REQUESTED, ACCEPTED, DRIVER_ARRIVED, IN_PROGRESS, COMPLETED, CANCELLED;
33
34 private static final Map<TripStatus, List<TripStatus>> TRANSITIONS = Map.of(
35 REQUESTED, List.of(ACCEPTED, CANCELLED),
36 ACCEPTED, List.of(DRIVER_ARRIVED, CANCELLED),
37 DRIVER_ARRIVED, List.of(IN_PROGRESS, CANCELLED),
38 IN_PROGRESS, List.of(COMPLETED),
39 COMPLETED, List.of(),
40 CANCELLED, List.of()
41 );
42
43 public boolean canTransitionTo(TripStatus next) {
44 return TRANSITIONS.getOrDefault(this, List.of()).contains(next);
45 }
46 }
47
48 public void transitionTrip(String tripId,
49 TripStatus from, TripStatus to) throws Exception {
50 if (!from.canTransitionTo(to))
51 throw new IllegalStateException(
52 "Invalid transition: " + from + " → " + to);
53
54 PreparedStatement ps = db.prepareStatement(
55 "UPDATE trips SET status=?, updated_at=NOW() " +
56 "WHERE trip_id=? AND status=?"
57 );
58 ps.setString(1, to.name());
59 ps.setString(2, tripId);
60 ps.setString(3, from.name());
61 if (ps.executeUpdate() == 0)
62 throw new RuntimeException(
63 "Concurrent state change detected — retry");
64 }
65}
661import java.sql.*;
2
3public class RideSharingSchema {
4 public static void main(String[] args) throws Exception {
5 Connection conn = DriverManager.getConnection(
6 "jdbc:postgresql://localhost/uber", "user", "pass"
7 );
8 Statement stmt = conn.createStatement();
9
10 stmt.execute(
11 "CREATE TABLE IF NOT EXISTS trips (" +
12 " trip_id UUID PRIMARY KEY," +
13 " rider_id UUID NOT NULL," +
14 " driver_id UUID," +
15 " status TEXT NOT NULL DEFAULT 'REQUESTED'," +
16 " pickup_lat DECIMAL(9,6)," +
17 " pickup_lng DECIMAL(9,6)," +
18 " dropoff_lat DECIMAL(9,6)," +
19 " dropoff_lng DECIMAL(9,6)," +
20 " fare_amount DECIMAL(8,2)," +
21 " surge_mult DECIMAL(4,2) DEFAULT 1.0," +
22 " requested_at TIMESTAMPTZ DEFAULT NOW()," +
23 " updated_at TIMESTAMPTZ DEFAULT NOW()" +
24 ");"
25 );
26 stmt.execute(
27 "CREATE INDEX IF NOT EXISTS idx_trips_rider " +
28 "ON trips(rider_id, requested_at DESC);"
29 );
30 stmt.execute(
31 "CREATE INDEX IF NOT EXISTS idx_trips_driver " +
32 "ON trips(driver_id, requested_at DESC);"
33 );
34 stmt.close();
35 conn.close();
36 }
37}
38Related Articles
React JS Interview Questions
Prepare for your React interview with the most asked questions for freshers and experienced developers. Covers hooks, lifecycle, performance optimization, and real-world scenarios.
FrontendJavaScript Interview Questions
Prepare for your next tech interview with the most asked JavaScript interview questions and answers. It includes basic to advanced concepts, coding problems, and real-world scenarios for freshers and experienced developers.