Five subjects.
One field manual.
A single page that walks you from the absolute basics to the depth interviewers actually probe — OOP, DBMS (with real SQL), Operating Systems, Computer Networks, and System Design. Built to be read top‑to‑bottom or jumped around mid‑prep.
OOP: how objects think
OOP models a program as a society of objects that hold their own data and expose behaviour. Master four pillars, then the two ideas interviewers love to poke at — polymorphism and the subtle line between overloading and overriding.
1.1 The four pillars
Everything else is built on these. Know the one-line definition and a concrete "why".
| Pillar | What it means | Why it matters |
|---|---|---|
| Abstraction | Expose what an object does, hide how. You drive a car via the pedal, not the combustion. | Lets callers depend on an interface, not internals. |
| Encapsulation | Bundle data + the methods that guard it; keep fields private behind getters/setters. | Invariants stay valid — no one sets age = -5. |
| Inheritance | A subclass reuses and specializes a parent (Dog extends Animal). | Share code, model "is-a" relationships. |
| Polymorphism | One interface, many behaviours — the same call resolves to different code per object. | Write code against the base type; new types slot in for free. |
Abstraction vs encapsulation get confused constantly. Abstraction is design-level (hiding complexity behind a clean interface); encapsulation is implementation-level (hiding data behind access modifiers). One is about what to show, the other about how to protect it.
1.2 Polymorphism: static vs dynamic
"Many forms." It comes in two flavours, and naming which is which is a guaranteed interview question.
- Compile-time / static polymorphism — resolved by the compiler using the method signature. This is method overloading (and, in C++, operator overloading).
- Run-time / dynamic polymorphism — resolved at runtime using the actual object's type. This is method overriding, powered by dynamic dispatch.
Overloading (compiler decides)
class Calc {
int add(int a, int b) { return a + b; } // baseline
double add(double a, double b) { return a + b; } // different param TYPES
int add(int a, int b, int c) { return a + b + c; } // different param COUNT
}
// The compiler looks at the ARGUMENTS you pass and picks the matching method.
new Calc().add(2, 3); // calls add(int,int)
new Calc().add(2.0, 3.0); // calls add(double,double)Overriding + dynamic dispatch (the object decides)
class Animal { void speak() { System.out.println("...generic..."); } }
class Dog extends Animal { @Override void speak() { System.out.println("Woof"); } }
class Cat extends Animal { @Override void speak() { System.out.println("Meow"); } }
Animal a = new Dog(); // reference TYPE = Animal, actual OBJECT = Dog
a.speak(); // prints "Woof" — chosen at RUNTIME from the real object
for (Animal x : new Animal[]{ new Dog(), new Cat() })
x.speak(); // "Woof" then "Meow" — same call site, different codeUnder the hood the JVM keeps a virtual method table (vtable) per class — a lookup of "for this method name, which actual implementation?" At the call a.speak() it follows the object's vtable, so the object's version runs regardless of the reference type.
In Java, instance methods are virtual by default — overriding "just works." In C++ you must opt in with virtual, otherwise the call binds statically to the reference type.
struct Animal { virtual void speak(); }; // 'virtual' enables dynamic dispatch
struct Dog : Animal { void speak() override; };
Animal* p = new Dog();
p->speak(); // WITH virtual -> Dog::speak(). WITHOUT virtual -> Animal::speak()1.3 Overloading vs overriding
The single most asked OOP question. Memorize this table cold.
| Overloading | Overriding | |
|---|---|---|
| Where | Same class (or inherited) | Across parent → child |
| Signature | Same name, different parameters | Identical name + parameters |
| Binding | Compile-time (static) | Run-time (dynamic) |
| Polymorphism | Static / ad-hoc | Dynamic / subtype |
| Return type | Can differ (but type alone ≠ overload) | Same, or a covariant subtype |
| Access modifier | Anything | Cannot be more restrictive |
| Exceptions | Anything | No new/broader checked exceptions |
static / final / private | Allowed | Cannot be overridden |
Return type alone does not overload. int f(int x) and double f(int x) won't compile together — the compiler can't tell them apart from the call site. You need a different parameter list.
main method?"A: Yes —
main(String[]) is just a method; you can add main(int) etc. But the JVM only ever calls public static void main(String[] args) as the entry point. The overloads are normal methods you'd have to call yourself.A: Yes — multiple constructors with different parameter lists. Constructors are never overridden (they aren't inherited), so only overloading applies.
1.4 Static members, hiding & binding
This is the "static inheritance" trap. Static members are inherited (a child can reference them), but they belong to the class, not the object — so they don't participate in dynamic dispatch. Redefining a static method in a child does not override it; it hides it.
class Parent {
static void who() { System.out.println("Parent.who [static]"); }
void hi() { System.out.println("Parent.hi [instance]"); }
}
class Child extends Parent {
static void who() { System.out.println("Child.who [static]"); } // HIDES
void hi() { System.out.println("Child.hi [instance]"); } // OVERRIDES
}
Parent p = new Child();
p.who(); // "Parent.who" -> STATIC resolved by REFERENCE type (compile time)
p.hi(); // "Child.hi" -> INSTANCE resolved by OBJECT type (runtime)Same story for fields — they are resolved by the reference type, never polymorphic. This is field hiding:
class A { int x = 1; }
class B extends A { int x = 2; } // hides A.x, doesn't override it
A ref = new B();
System.out.println(ref.x); // 1 -> field picked by REFERENCE type A
System.out.println(((B) ref).x); // 2 -> cast changes the reference typeOne sentence to carry into the room: instance methods bind dynamically (object type); static methods and all fields bind statically (reference type). Override applies only to non-static, non-final, non-private instance methods.
1.5 Design patterns you must know
Patterns are reusable solutions to recurring design problems. They cluster into three families.
| Family | Solves | Common members |
|---|---|---|
| Creational | How objects get made | Singleton, Factory Method, Abstract Factory, Builder, Prototype |
| Structural | How objects compose | Adapter, Decorator, Facade, Proxy, Composite |
| Behavioral | How objects talk & share responsibility | Strategy, Observer, Command, State, Iterator, Template Method |
Singleton — exactly one instance
// (a) Bill Pugh holder — lazy, thread-safe, no locking. PREFERRED.
public class Config {
private Config() {}
private static class Holder { static final Config INSTANCE = new Config(); }
public static Config get() { return Holder.INSTANCE; } // class loaded only on first use
}
// (b) Double-checked locking — note 'volatile' (prevents seeing a half-built object)
public class Cache {
private static volatile Cache instance;
private Cache() {}
public static Cache get() {
if (instance == null) { // 1st check (no lock, fast path)
synchronized (Cache.class) {
if (instance == null) // 2nd check (inside lock)
instance = new Cache();
}
}
return instance;
}
}
// (c) Enum — simplest; immune to reflection & serialization attacks
public enum Settings { INSTANCE; public void load() { /* ... */ } }Without volatile, double-checked locking is broken: instruction reordering can publish the reference before the constructor finishes, so another thread sees a non-null but half-initialized object.
Factory Method — ask for a type, get an object
interface Shape { double area(); }
class Circle implements Shape { public double area() { return 3.14159; } }
class Square implements Shape { public double area() { return 4.0; } }
class ShapeFactory {
static Shape create(String type) {
return switch (type) {
case "circle" -> new Circle();
case "square" -> new Square();
default -> throw new IllegalArgumentException("Unknown: " + type);
};
}
}
Shape s = ShapeFactory.create("circle"); // caller never says 'new Circle()'Builder — readable construction of complex objects
class Pizza {
private final String size; private final boolean cheese, pepperoni;
private Pizza(Builder b) { size = b.size; cheese = b.cheese; pepperoni = b.pepperoni; }
static class Builder {
private final String size; // required
private boolean cheese, pepperoni; // optional
Builder(String size) { this.size = size; }
Builder cheese() { this.cheese = true; return this; } // fluent
Builder pepperoni() { this.pepperoni = true; return this; }
Pizza build() { return new Pizza(this); }
}
}
Pizza p = new Pizza.Builder("large").cheese().pepperoni().build();Builder kills the "telescoping constructor" anti-pattern (new Pizza("L", true, false, true, ...) where nobody can tell what the booleans mean).
Strategy — swap an algorithm at runtime
interface PayStrategy { void pay(int amount); }
class CardPay implements PayStrategy { public void pay(int a) { /* charge card */ } }
class UpiPay implements PayStrategy { public void pay(int a) { /* charge UPI */ } }
class Checkout {
private PayStrategy strategy;
void use(PayStrategy s) { this.strategy = s; } // inject behaviour
void checkout(int amount) { strategy.pay(amount); }
}
Checkout c = new Checkout();
c.use(new UpiPay()); // pick the algorithm without touching Checkout's code
c.checkout(500);Observer — publish/subscribe
interface Observer { void update(String news); }
class NewsAgency { // the Subject
private final List<Observer> subs = new ArrayList<>();
void subscribe(Observer o) { subs.add(o); }
void publish(String news) { for (Observer o : subs) o.update(news); }
}
// Any number of observers register and get pushed updates — loose coupling.Decorator — add behaviour by wrapping
interface Coffee { double cost(); }
class Plain implements Coffee { public double cost() { return 40; } }
abstract class AddOn implements Coffee { // wraps another Coffee
protected final Coffee inner;
AddOn(Coffee c) { this.inner = c; }
}
class Milk extends AddOn {
Milk(Coffee c) { super(c); }
public double cost() { return inner.cost() + 10; } // extend without subclass-explosion
}
Coffee order = new Milk(new Milk(new Plain())); // 40 + 10 + 10 = 60A: Structurally near-identical, but intent differs. Strategy = the client picks an interchangeable algorithm. State = the object changes its own behaviour as its internal state transitions (and states often trigger the next state).
A: It's global mutable state in disguise — it hides dependencies, makes unit testing hard (can't easily mock/replace it), and can become a concurrency bottleneck. Dependency injection is usually cleaner.
DBMS: keeping data correct under chaos
A database's whole job is to stay correct while many users hammer it at once and machines crash mid-write. That promise is ACID, enforced by transactions, concurrency control, and recovery. Then we get hands-on with SQL.
2.1 Transactions & ACID
A transaction is a unit of work that must happen completely or not at all — classically, transferring money: debit one account, credit another. Halfway = corruption. Every transaction guarantees four properties:
| Property | Promise | Enforced by |
|---|---|---|
| A — Atomicity | All operations commit, or all roll back. No partial work. | Undo log, COMMIT/ROLLBACK |
| C — Consistency | The DB moves from one valid state to another; constraints (keys, checks) always hold. | Constraints + the app's correct logic |
| I — Isolation | Concurrent transactions don't see each other's half-done work; result equals some serial order. | Locks / MVCC |
| D — Durability | Once committed, data survives crashes and power loss. | Write-ahead (redo) log on disk |
BEGIN; -- start transaction
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- debit
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- credit
COMMIT; -- both succeed together; if anything fails -> ROLLBACK (atomicity)2.2 Concurrency control
Run transactions in parallel for speed, but interleaving creates anomalies. Interviewers expect you to name all four:
| Anomaly | What happens |
|---|---|
| Lost update | T1 and T2 both read X, both write; the later write silently erases the earlier one. |
| Dirty read | T1 reads a value T2 wrote but hasn't committed; T2 then rolls back — T1 used data that never existed. |
| Non-repeatable read | T1 reads a row, T2 updates+commits it, T1 reads again and gets a different value. |
| Phantom read | T1 runs a range query, T2 inserts a new matching row, T1 re-runs and sees an extra row. |
Isolation levels — the dial that trades safety for speed
| Level | Dirty read | Non-repeatable | Phantom |
|---|---|---|---|
| Read Uncommitted | possible | possible | possible |
| Read Committed | prevented | possible | possible |
| Repeatable Read | prevented | prevented | possible* |
| Serializable | prevented | prevented | prevented |
*MySQL's InnoDB actually blocks phantoms at Repeatable Read too, using gap locks. Higher isolation = more correctness but more locking and less concurrency.
How isolation is implemented
- Lock-based — shared (S) locks for reads, exclusive (X) locks for writes. The key protocol is Two-Phase Locking (2PL): a growing phase where a txn only acquires locks, then a shrinking phase where it only releases. 2PL guarantees conflict serializability.
- Strict 2PL — hold all exclusive locks until
COMMIT. Prevents cascading rollbacks (no one reads your uncommitted writes). - Timestamp ordering — each txn gets a timestamp; conflicting operations must execute in timestamp order, else the younger txn aborts.
- MVCC (multi-version concurrency control) — every write creates a new version; readers see a consistent snapshot from their start time. Readers never block writers and vice-versa (used by PostgreSQL, InnoDB, Oracle).
A schedule is conflict-serializable if its precedence graph (node per txn, edge on each conflicting read/write pair) is acyclic. Acyclic ⇒ equivalent to some serial order ⇒ safe.
Locking introduces deadlocks: T1 holds lock on A and wants B; T2 holds B and wants A. The DB detects the cycle in its wait-for graph (or hits a timeout) and aborts one transaction as the "victim."
2.3 Atomicity & recovery
How does "all-or-nothing" survive a power cut mid-write? The answer is Write-Ahead Logging (WAL): the change is written to a durable log before it touches the actual data pages.
- Undo info lets the DB roll back uncommitted transactions after a crash (atomicity).
- Redo info lets it re-apply committed transactions that hadn't reached disk (durability).
SAVEPOINTgives partial rollback inside a transaction.
BEGIN;
INSERT INTO orders(id, total) VALUES (1, 500);
SAVEPOINT after_order;
INSERT INTO items(order_id, sku) VALUES (1, 'BAD');
ROLLBACK TO after_order; -- undo just the items insert, keep the order
COMMIT;A: The recovery manager + WAL. Every modification logs an undo (old value) and redo (new value) record before the data page is flushed. On restart, the DB replays the log: redo committed work, undo anything uncommitted. So a transaction is either fully reflected or fully erased.
2.4 Redundancy vs consistency
Store the same fact in two places and they will drift apart. Redundancy causes anomalies:
- Update anomaly — change a department name, miss one row, now it's inconsistent.
- Insertion anomaly — can't add a department until at least one employee exists in it.
- Deletion anomaly — delete the last employee and you accidentally lose the department's info.
Normalization removes redundancy by splitting tables according to functional dependencies (X → Y means X determines Y).
| Form | Rule | Removes |
|---|---|---|
| 1NF | Atomic values only; no repeating groups / arrays in a cell. | Multi-valued cells |
| 2NF | 1NF + no partial dependency (every non-key column depends on the whole composite key). | Partial dependencies |
| 3NF | 2NF + no transitive dependency (non-key columns depend only on the key, not on other non-key columns). | Transitive dependencies |
| BCNF | For every dependency X → Y, X must be a candidate key. (Stricter 3NF.) | Remaining key anomalies |
The 3NF mantra: "every non-key attribute depends on the key, the whole key, and nothing but the key." ("the key" = 1NF, "the whole key" = 2NF, "nothing but the key" = 3NF.)
Denormalization is the deliberate reverse: re-introduce redundancy (e.g. store dept_name on each order) to avoid expensive joins and speed up reads. The trade: faster reads, but slower/riskier writes and a duty to keep copies in sync. Normalize for OLTP correctness; denormalize selectively for read-heavy analytics.
2.5 SQL — from zero to interview queries
SQL has four sub-languages. Knowing which is which signals fluency:
Our example schema (used throughout)
CREATE TABLE departments (
id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL
);
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
dept_id INT,
manager_id INT, -- self-reference to another employee's id
salary INT,
FOREIGN KEY (dept_id) REFERENCES departments(id),
FOREIGN KEY (manager_id) REFERENCES employees(id)
);SELECT — the workhorse
SELECT name, salary -- 1. which columns to return
FROM employees -- 2. from which table
WHERE salary >= 50000 -- 3. keep only matching rows
ORDER BY salary DESC -- 4. sort the result
LIMIT 10; -- 5. cap the number of rowsSQL is written SELECT-first but executed in this order:
FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT
That's why you can't use a SELECT alias in WHERE (WHERE runs before SELECT exists) but can use it in ORDER BY (which runs after).
JOINs — combining tables
A join matches rows across tables on a condition. The type decides what happens to non-matching rows.
| Join | Returns |
|---|---|
| INNER JOIN | Only rows with a match in both tables. |
| LEFT JOIN | All left rows; right columns are NULL where no match. |
| RIGHT JOIN | All right rows; left columns NULL where no match. |
| FULL OUTER JOIN | All rows from both; NULLs on the side that's missing. |
| CROSS JOIN | Cartesian product — every left row × every right row. |
| SELF JOIN | A table joined to itself (e.g. employee ↔ manager). |
-- INNER: employees that have a department
SELECT e.name, d.name AS dept
FROM employees e
JOIN departments d ON e.dept_id = d.id;
-- LEFT: ALL employees, dept shows NULL if they have none
SELECT e.name, d.name AS dept
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.id;
-- SELF JOIN: pair each employee with their manager
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;Aggregation: GROUP BY & HAVING
Aggregate functions (COUNT, SUM, AVG, MIN, MAX) collapse many rows into one. GROUP BY makes one result row per group.
SELECT dept_id,
COUNT(*) AS headcount,
AVG(salary) AS avg_pay
FROM employees
GROUP BY dept_id
HAVING AVG(salary) > 60000 -- filter GROUPS (after aggregation)
ORDER BY avg_pay DESC;WHERE filters rows before grouping; HAVING filters groups after aggregation. You cannot put an aggregate in WHERE (WHERE COUNT(*) > 1 is illegal) — that's HAVING's job. Also: every non-aggregated column in SELECT must appear in GROUP BY.
Subqueries
-- Scalar subquery: who earns above the company average?
SELECT name FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
-- Correlated subquery: above-average WITHIN their own department
-- (inner query re-runs for each outer row, referencing e.dept_id)
SELECT e.name
FROM employees e
WHERE e.salary > (SELECT AVG(x.salary) FROM employees x WHERE x.dept_id = e.dept_id);
-- EXISTS: departments that have at least one employee
SELECT d.name FROM departments d
WHERE EXISTS (SELECT 1 FROM employees e WHERE e.dept_id = d.id);Window functions (the senior-level differentiator)
Like aggregates, but they keep every row and compute across a "window" of related rows defined by OVER (PARTITION BY ... ORDER BY ...).
SELECT name, dept_id, salary,
ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rnk,
DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS drnk,
SUM(salary) OVER (PARTITION BY dept_id) AS dept_total,
LAG(salary) OVER (ORDER BY salary) AS prev_salary
FROM employees;For salaries 100, 100, 90: ROW_NUMBER = 1, 2, 3 (always unique). RANK = 1, 1, 3 (skips after a tie). DENSE_RANK = 1, 1, 2 (no gaps).
Classic interview queries
1 · Nth-highest salary (shown for 2nd) — three idiomatic ways:
-- (a) Subquery — max salary that is below the overall max
SELECT MAX(salary) FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);
-- (b) DENSE_RANK — generalises to ANY N and handles ties cleanly
SELECT DISTINCT salary FROM (
SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS r
FROM employees
) t
WHERE r = 2; -- change 2 to N
-- (c) LIMIT / OFFSET (MySQL, Postgres)
SELECT DISTINCT salary FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET 1; -- OFFSET = N - 12 · Find duplicates and 3 · earn more than manager:
-- Duplicate names
SELECT name, COUNT(*) AS cnt
FROM employees
GROUP BY name
HAVING COUNT(*) > 1;
-- Employees who out-earn their manager (self-join)
SELECT e.name
FROM employees e
JOIN employees m ON e.manager_id = m.id
WHERE e.salary > m.salary;4 · Top earner per department and 5 · delete duplicates keeping one:
-- Highest-paid employee in each department
SELECT name, dept_id, salary FROM (
SELECT name, dept_id, salary,
RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS r
FROM employees
) t
WHERE r = 1;
-- Remove duplicate rows, keep the one with the smallest id
DELETE FROM employees
WHERE id NOT IN (
SELECT MIN(id) FROM employees GROUP BY name, dept_id, salary
);Indexes — why queries get fast (or slow)
An index is an auxiliary structure (usually a B+ tree) that keeps a column's values sorted with pointers to rows — turning a full-table scan O(n) into a tree lookup O(log n) for WHERE, JOIN, and ORDER BY.
CREATE INDEX idx_emp_dept ON employees(dept_id); -- speeds up filters/joins on dept_idDon't index everything. Each index costs storage and slows every INSERT/UPDATE/DELETE (the index must be maintained). Index high-selectivity columns used in filters and joins. A clustered index sorts the table itself (one per table); a non-clustered index is a separate structure pointing back to rows.
OS: sharing one machine, safely
An OS multiplexes finite hardware — CPU, RAM, devices — across many programs while keeping them isolated and responsive. The interview core: how concurrent work is coordinated (semaphores), how it deadlocks, and how virtual memory fakes infinite RAM.
3.1 Process vs thread
A process is a program in execution with its own address space and resources. A thread is the smallest unit of execution inside a process; multiple threads share the process's memory but each has its own stack, registers, and program counter.
| Process | Thread | |
|---|---|---|
| Address space | Private, isolated | Shared with sibling threads |
| Owns | Code, heap, data, files, PCB | Stack, registers, PC, TCB |
| Creation / switch | Heavy (new space, TLB flush) | Light (same space) |
| Communication | IPC: pipes, sockets, message queues, shared memory | Direct — shared variables |
| Fault isolation | One crash doesn't kill others | A bad thread can crash the whole process |
| Sync needed? | Rarely (isolated) | Constantly (shared data → races) |
Threads buy concurrency cheaply by sharing memory — but that shared memory is exactly why you need locks/semaphores. Processes are safer but pricier to create and to communicate between.
3.2 Semaphores
A semaphore is an integer guarded by two atomic operations:
wait()(a.k.a.P/down): decrement; if the result would be negative, the caller blocks until a resource frees up.signal()(a.k.a.V/up): increment; wake one waiting thread if any.
Binary semaphore
Value 0 or 1. Acts like a lock for mutual exclusion / signaling.
Counting semaphore
Any non-negative value. Controls access to a pool of N identical resources.
They look alike but differ in ownership. A mutex has an owner: only the thread that locked it may unlock it (built for mutual exclusion). A semaphore has no owner: any thread can signal() it (built for signaling/counting). Using a semaphore where you need a mutex is a classic bug.
The canonical problem: producer–consumer
A bounded buffer of size N. Producers must wait if it's full; consumers must wait if it's empty; only one thread touches the buffer at a time. Three semaphores solve it:
semaphore mutex = 1; // mutual exclusion on the buffer
semaphore empty = N; // number of empty slots (producers wait on this)
semaphore full = 0; // number of filled slots (consumers wait on this)
void producer() {
while (true) {
item = produce();
wait(empty); // is there room? block if buffer full
wait(mutex); // enter critical section
buffer_put(item);
signal(mutex); // leave critical section
signal(full); // announce: one more item to consume
}
}
void consumer() {
while (true) {
wait(full); // is there an item? block if buffer empty
wait(mutex);
item = buffer_get();
signal(mutex);
signal(empty); // announce: one more free slot
consume(item);
}
}Order matters: take the counting semaphore (empty/full) before mutex. Reverse it and you can deadlock — a producer holding mutex while blocked on a full buffer freezes every consumer that needs mutex to make room.
3.3 Deadlock
A deadlock is a set of processes each waiting forever for a resource another holds. It can happen only when all four Coffman conditions hold at once:
| Condition | Meaning | Break it by… |
|---|---|---|
| Mutual exclusion | A resource is non-shareable (one holder at a time). | Make resources shareable where possible |
| Hold and wait | A process holds resources while requesting more. | Request all resources up front |
| No preemption | Resources can't be forcibly taken; released only voluntarily. | Allow preemption / rollback |
| Circular wait | A cycle: P1 waits for P2's resource, … Pn waits for P1's. | Impose a global ordering on resources |
The four strategies
- Prevention — structurally negate one of the four conditions (e.g. always acquire resources in a fixed numeric order ⇒ no circular wait).
- Avoidance — grant a request only if the system stays in a safe state. The Banker's algorithm checks whether a safe sequence still exists (one ordering in which every process can get its max need and finish) before allocating.
- Detection & recovery — allow deadlocks, find cycles in the wait-for graph, then recover by aborting a process or preempting resources.
- Ignore (Ostrich algorithm) — pretend it won't happen; reboot if it does. What general-purpose OSes mostly do, because prevention is costly and deadlocks are rare.
A: Deadlock — processes blocked forever in a cycle, none progresses. Starvation — a process is perpetually denied a resource (e.g. low priority always skipped) though others progress. Livelock — processes keep changing state in response to each other but make no real progress (two people stepping aside in a hallway forever).
3.4 Virtual memory
Virtual memory gives each process its own large, contiguous address space — even larger than physical RAM — by splitting memory into fixed-size pages (virtual) mapped to frames (physical) via a page table. The MMU translates every address; rarely-used pages live on disk.
- Demand paging — a page is loaded only when first accessed; the access traps as a page fault and the OS fetches it.
- TLB (Translation Lookaside Buffer) — a small cache of recent page→frame translations, so most lookups skip the page table entirely.
- Locality of reference — programs tend to reuse nearby addresses, which is why caching and paging work at all.
Page replacement — what to evict when RAM is full
| Algorithm | Evicts… | Note |
|---|---|---|
| FIFO | The oldest-loaded page | Simple, but suffers Belady's anomaly |
| Optimal (OPT) | The page used farthest in the future | Theoretical best; needs the future, so unimplementable — a benchmark |
| LRU | The least-recently-used page | Good real-world choice; approximates OPT via the past |
Intuitively, more frames ⇒ fewer faults. With FIFO this can reverse: adding frames sometimes increases page faults. LRU and OPT are "stack algorithms" and never show this anomaly.
When the active working set doesn't fit in RAM, the system spends nearly all its time paging in/out instead of computing — that's thrashing. CPU utilization collapses. The fix: reduce the degree of multiprogramming (fewer processes) or add RAM so each working set fits.
Networks: getting bits across the world
Networking is layered so each level solves one problem and trusts the layer below. Nail the layer model and its protocols, the TCP vs UDP trade-off, how errors are caught/fixed, and how routers find paths.
4.1 The layers & their protocols
The OSI model has 7 conceptual layers; the practical TCP/IP model collapses them into 4. Data gains a header at each layer going down (encapsulation) and sheds it going up.
| OSI layer | Job | Protocols / devices | Unit |
|---|---|---|---|
| 7 Application | Services the user touches | HTTP(S), DNS, FTP, SMTP, DHCP | Data |
| 6 Presentation | Encoding, encryption, compression | TLS/SSL, JPEG, ASCII | Data |
| 5 Session | Open / manage / close sessions | RPC, NetBIOS | Data |
| 4 Transport | End-to-end delivery, ports | TCP, UDP | Segment |
| 3 Network | Logical addressing & routing | IP, ICMP · routers | Packet |
| 2 Data Link | Node-to-node, MAC, framing | Ethernet, ARP · switches | Frame |
| 1 Physical | Raw bits on the medium | Cables, radio, signals | Bit |
TCP/IP mapping: Application (OSI 5–7) · Transport (4) · Internet (3) · Link/Network Access (1–2). Mnemonic, bottom-up: Please Do Not Throw Sausage Pizza Away.
MAC addresses are Layer 2 (physical, burned into the NIC, local hop). IP addresses are Layer 3 (logical, routable end-to-end). Port numbers are Layer 4 (which application/socket). ARP is what maps an IP to a MAC on the local network.
4.2 TCP vs UDP
Both are Layer-4 transport protocols; the choice is reliability vs speed. This comparison is asked in nearly every networking interview.
| TCP | UDP | |
|---|---|---|
| Connection | Connection-oriented (handshake first) | Connectionless (just send) |
| Reliability | Guaranteed, ordered, retransmits lost data | Best-effort — may drop, duplicate, reorder |
| Flow / congestion control | Yes (windowing, slow start) | No |
| Header size | 20+ bytes (heavier) | 8 bytes (lean) |
| Speed | Slower, more overhead | Fast, low latency |
| Use it for | Web, email, file transfer, anything that must arrive intact | Live video/voice, gaming, DNS — where speed beats perfection |
To open a connection: SYN (client → server, "let's talk, seq=x") → SYN-ACK (server → client, "ok, seq=y, ack=x+1") → ACK (client → server, "ack=y+1"). Now both sides agree on starting sequence numbers. Teardown takes four steps (FIN/ACK each direction).
4.3 Error management
Bits flip in transit. The receiver must at least detect corruption, and sometimes correct it.
Detection — "is this frame damaged?"
| Scheme | How | Catches |
|---|---|---|
| Parity bit | Add 1 bit so the count of 1s is even (or odd). | Any odd number of flipped bits |
| 2D parity | Parity per row and per column. | Detects + can correct a single-bit error |
| Checksum | Sum the data words (one's-complement), send the sum; receiver re-adds. | Many errors; weaker than CRC |
| CRC | Treat data as a polynomial, divide by a generator, append the remainder. | Very strong — esp. burst errors (used in Ethernet) |
Correction — "fix it without re-asking"
Hamming code places parity bits at power-of-2 positions (1, 2, 4, 8…). Their combined "syndrome" pinpoints the exact position of a single flipped bit, so the receiver flips it back — single-error correction with no retransmission.
When you'd rather just ask again, you use ARQ (Automatic Repeat reQuest) — retransmission protocols built on ACKs and timeouts:
| ARQ protocol | Behaviour | Efficiency |
|---|---|---|
| Stop-and-Wait | Send one frame, wait for its ACK before sending the next. | Low on high-latency links (line sits idle) |
| Go-Back-N | Send a window of N frames; on a loss, retransmit that frame and everything after it. Receiver discards out-of-order frames. | Better; wastes bandwidth re-sending good frames |
| Selective Repeat | Retransmit only the lost frame; receiver buffers out-of-order ones. | Best; needs buffering & more bookkeeping |
4.4 Routing basics
Routing is how routers decide the path a packet takes across networks, using routing tables they build and update. Dynamic routing splits into two big families:
Distance Vector
Each router tells neighbors its distance to every destination; "routing by rumor." Computes paths with the Bellman-Ford algorithm.
Protocol: RIP (hop-count metric, max 15). Weakness: the count-to-infinity problem — slow to learn a link died. Mitigated by split horizon & poison reverse.
Link State
Each router floods the entire topology (link-state advertisements) to everyone, then independently runs Dijkstra's algorithm to find shortest paths.
Protocol: OSPF. Trade: faster convergence and loop-free, but more memory and CPU per router.
Within an organization you run an interior protocol (RIP / OSPF). Between organizations (autonomous systems), the internet runs BGP — a path-vector protocol that exchanges full AS-paths and applies policy, not just shortest distance.
A: Browser checks cache → DNS resolves the domain to an IP (Layer 7 over UDP) → TCP 3-way handshake to that IP:443 → TLS handshake for HTTPS → HTTP request sent → server responds → browser parses HTML, fetches assets, renders. Along the way IP routing (Layer 3) and ARP/MAC (Layer 2) move each packet hop by hop.
System Design: from one class to a million users
Two scales of the same skill. LLD is about classes, responsibilities and relationships inside one service — interviewers probe it through SOLID, UML and patterns. HLD is about boxes and arrows between services — load balancers, caches, databases, queues — and the trade-offs that hold them together. Learn the vocabulary, then the framework to apply it under pressure.
5.1 SOLID & Low-Level Design
SOLID is five principles for writing classes that are easy to change without breaking things. They are the backbone of every LLD interview — know the name, the one-line idea, and the smell each one fixes.
| Letter | Principle | In one line |
|---|---|---|
| S | Single Responsibility | A class should have one reason to change — one job. A User class shouldn't also format reports and send email. |
| O | Open/Closed | Open for extension, closed for modification. Add new behaviour by adding code, not editing tested code. |
| L | Liskov Substitution | A subclass must be usable anywhere its parent is, without surprises. The classic Square extends Rectangle trap breaks this. |
| I | Interface Segregation | Many small, focused interfaces beat one fat one. Don't force a class to implement methods it doesn't use. |
| D | Dependency Inversion | Depend on abstractions, not concretions. High-level code shouldn't new up low-level classes — inject interfaces. |
The O and D principles are where most refactors happen. Here's the Open/Closed idea concretely — a payment processor that breaks the rule, then one that follows it:
// ✗ Violates OCP: every new method forces editing this class
class PaymentService {
void pay(String type) {
if (type.equals("card")) { /* ... */ }
else if (type.equals("upi")) { /* ... */ } // edit again for each new type
}
}
// ✓ Follows OCP + DIP: extend by adding a class, depend on the interface
interface PaymentMethod { void pay(double amount); }
class CardPayment implements PaymentMethod {
public void pay(double amount) { /* charge card */ }
}
class UpiPayment implements PaymentMethod {
public void pay(double amount) { /* charge UPI */ }
}
class PaymentService {
void checkout(PaymentMethod method, double amount) {
method.pay(amount); // new methods need ZERO changes here
}
}Reciting "SOLID" but only being able to expand S. Interviewers always push: "give me an example of an LSP violation." Have one ready — the Square/Rectangle case (setting width on a Square silently changes height, breaking code written against Rectangle) or a Bird hierarchy where Penguin.fly() has to throw.
The other principles you'll be quizzed on
DRY says extract duplicated logic into one place. KISS and YAGNI push the other way — don't over-engineer or build for imaginary future needs. Composition over inheritance means prefer "has-a" (a Car has an Engine) over deep "is-a" trees, because inheritance is rigid and leaks parent details into children. The Strategy and Decorator patterns are composition in action.
Design patterns — the LLD recap map
You met several patterns in the OOP section. Interviewers expect you to place a pattern in its family and name when you'd reach for it. The three Gang-of-Four families:
| Family | Solves | Patterns (★ = covered earlier) |
|---|---|---|
| Creational | How objects get made | ★ Singleton, ★ Factory Method, ★ Builder, Abstract Factory, Prototype |
| Structural | How objects are composed into bigger structures | ★ Decorator, Adapter, Facade, Proxy, Composite, Bridge |
| Behavioural | How objects communicate & share responsibility | ★ Strategy, ★ Observer, Command, Template Method, State, Iterator, Chain of Responsibility |
Pattern questions are usually "which pattern fits X?" Quick reflexes: pluggable algorithms → Strategy; notify many on change → Observer; wrap to add behaviour → Decorator; make incompatible interfaces work together → Adapter; simplify a messy subsystem behind one entry point → Facade; build a complex object step-by-step → Builder; one shared instance → Singleton. Don't force patterns where a plain method works — that's an anti-pattern in itself.
5.2 UML relationships
LLD answers are drawn as class diagrams, so you must read and draw the relationship arrows. They differ by strength of coupling and lifetime ownership:
| Relationship | Meaning | Lifetime | Example |
|---|---|---|---|
| Association ───> | "uses-a" / knows-about. A general link between two classes. | Independent | A Teacher is associated with Students. |
| Aggregation ◇─── | "has-a", weak ownership. The whole holds parts, but parts can outlive it. | Independent | A Department has Professors — close the dept, profs still exist. |
| Composition ◆─── | "owns-a", strong ownership. Parts can't exist without the whole. | Tied together | A House has Rooms — destroy the house, rooms go too. |
| Inheritance ──▷ | "is-a". A subclass specialises a superclass. | — | Car is a Vehicle. |
| Dependency - - -> | "depends-on" transiently — appears as a method parameter or local, not a field. | Momentary | OrderService depends on a PaymentGateway passed to checkout(). |
Aggregation vs Composition trips everyone up. The test is lifetime: if destroying the container should destroy the parts, it's composition (filled diamond ◆). If the parts can live on independently, it's aggregation (hollow diamond ◇). Both are "has-a"; ownership strength is the difference.
A worked mini-LLD: a parking lot
This is the canonical LLD warm-up. You don't write full code — you identify entities, relationships and key behaviours. A clean first pass:
ParkingLot ──◆── Floor // lot OWNS floors (composition)
Floor ──◆── ParkingSpot // floor OWNS spots
ParkingSpot ──> Vehicle // spot references the parked vehicle
Vehicle (abstract) // is-a hierarchy
├── Car extends Vehicle
├── Bike extends Vehicle
└── Truck extends Vehicle
SpotType enum { COMPACT, LARGE, BIKE, EV } // strategy for fit-check
Ticket { id, spot, entryTime, exitTime }
FeeStrategy (interface) // ✦ Strategy pattern: hourly / flat / EV
ParkingLot.assignSpot(Vehicle) // key behaviour → returns Ticket
ParkingLot.processExit(Ticket) // → FeeStrategy.calculate()Notice the SOLID/pattern hooks: FeeStrategy keeps pricing open/closed, the Vehicle hierarchy uses inheritance, and the lot→floor→spot chain is composition. Calling those out by name is exactly what scores points.
5.3 HLD building blocks
High-Level Design zooms out to services, data stores and the network between them. There's a fixed toolbox of components; an HLD answer is choosing the right ones and justifying the trade-offs. Start with the most fundamental choice — how you grow.
Scaling: vertical vs horizontal
Vertical (scale up)
Buy a bigger machine — more CPU/RAM. Simple, no code changes. But there's a hard ceiling, it's expensive, and that one box is a single point of failure.
Horizontal (scale out)
Add more machines and split the load. Near-unlimited and fault-tolerant, but needs a load balancer, and your app must be stateless (or share state externally) to spread freely.
Load balancing
A load balancer sits in front of your servers and distributes incoming requests so no single server is overwhelmed. It also does health checks — routing traffic away from dead instances. Common algorithms: round-robin (rotate evenly), least-connections (send to the least-busy server), and consistent hashing (stick a given user/key to the same server, vital for caches).
Caching
A cache stores hot data in fast memory (e.g. Redis, Memcached) so you avoid slow recomputes or DB hits. Caching is the single highest-leverage performance lever — and the source of the hardest bugs (stale data, invalidation).
| Concept | What it is |
|---|---|
| Cache-aside (lazy) | App checks cache first; on a miss, reads DB and populates the cache. Most common. |
| Write-through | Writes go to cache and DB together — cache always fresh, writes slightly slower. |
| Write-back | Write to cache now, flush to DB later — fast, but risks data loss on crash. |
| Eviction policy | When full, what to drop: LRU (least recently used) is the default; also LFU, FIFO. |
| TTL | Time-to-live — entries auto-expire to bound staleness. |
| CDN | Content Delivery Network — caches static assets (images, JS, CSS) at edge servers physically near users. |
Forgetting cache invalidation. "There are only two hard things in CS: cache invalidation and naming things." When the underlying data changes, the cached copy is now stale. Always state your invalidation strategy (TTL expiry, or explicit delete-on-write) — interviewers wait for it.
Database scaling: replication & sharding
Replication
Copy data across multiple DB nodes. Primary-replica: writes go to the primary, reads spread across read-replicas — great for read-heavy loads. Adds replication lag (replicas briefly behind).
Sharding (partitioning)
Split one big dataset across DBs by a shard key (e.g. user_id % N, or by region). Each shard holds a slice. Scales writes too, but cross-shard queries and joins get painful, and a bad key causes hot shards.
Partitioning comes in two flavours: horizontal (split rows across stores — what "sharding" usually means) and vertical (split columns/tables by access pattern).
SQL vs NoSQL
| SQL (relational) | NoSQL | |
|---|---|---|
| Schema | Fixed, predefined | Flexible / schema-less |
| Scaling | Vertical mainly; sharding is manual | Built for horizontal scale-out |
| Guarantees | Strong ACID, joins, complex queries | Often BASE; eventual consistency, denormalized |
| Use when | Relationships, transactions, reporting (banking, orders) | Massive scale, flexible/changing data (feeds, logs, catalogs) |
| Types | Postgres, MySQL | Document (Mongo), key-value (Redis/DynamoDB), wide-column (Cassandra), graph (Neo4j) |
CAP theorem
The cornerstone trade-off of distributed data. A distributed store can guarantee only two of these three at once:
Since network partitions will happen at scale, P is non-negotiable — so the real choice is C vs A. A CP system (e.g. a banking ledger) refuses to answer rather than return stale data; an AP system (e.g. a social feed) always answers, accepting it may be briefly out of date.
The sharper modern framing is PACELC: if Partitioned, choose A or C; Else (normal operation) choose Latency or Consistency. It captures that even with no partition, you still trade speed against strong consistency. Naming PACELC signals depth beyond the textbook CAP triangle.
Async & the rest of the toolbox
| Component | Why it exists |
|---|---|
| Message queue (Kafka, RabbitMQ, SQS) | Decouples producer from consumer. Smooths traffic spikes, enables async work (email, video encoding), and buffers so a slow consumer doesn't drop requests. |
| API Gateway | Single entry point for clients — handles routing, auth, rate limiting, and request aggregation in front of many microservices. |
| Rate limiter | Caps requests per client (e.g. token-bucket / leaky-bucket algorithm) to prevent abuse and protect downstream services. |
| Reverse proxy | Fronts servers for SSL termination, compression, and caching (e.g. Nginx). |
| Blob/object store (S3) | Stores large files (images, video, backups) cheaply — DBs are the wrong place for these. |
Microservices vs monolith
Monolith
One deployable unit. Simple to build, test and deploy early; fast in-process calls. But it gets tangled at scale and you must redeploy everything for any change.
Microservices
Many small services, deployed independently, each owning its data. Scale and ship teams independently — at the cost of network latency, distributed-system complexity, and harder debugging. Start monolith, split when pain is real.
Interviewers want rough numbers, not precision. Anchor on a few constants: 1 day ≈ 86,400 s ≈ 10⁵ s; a read from memory ≈ 100 ns, from SSD ≈ 100 µs, from disk/network ≈ 1–100 ms. Method: users → requests/day → divide by 10⁵ for QPS → ×payload for bandwidth → ×retention for storage. Example: 100M daily users × 10 requests = 10⁹/day ÷ 10⁵ ≈ 10k QPS average (×2–3 for peak). Always separate read QPS from write QPS — it drives your whole design.
5.4 The interview framework
A system-design round is open-ended on purpose — they're watching your process. Don't jump to drawing databases. Follow a script so you never freeze. The standard 6-step flow:
- Clarify requirements (~5 min). Pin down functional needs ("users post and follow") and non-functional ones (scale, latency, consistency vs availability, read:write ratio). Never design before scoping — ask first.
- Estimate scale. Back-of-envelope the QPS, storage, and bandwidth from the numbers above. This justifies every later choice (do we even need sharding?).
- Define the API. List the core endpoints —
createPost(),getFeed(userId). This nails down exactly what the system must do. - Design the data model. Pick SQL vs NoSQL with a reason, sketch the main tables/entities and the access patterns that shape them.
- Draw the high-level design. Client → load balancer → app servers → cache → DB, plus queues/CDN/blob store as needed. Walk a request through end to end.
- Deep-dive & address bottlenecks. Pick the hard part (the feed fan-out, the hot shard) and go deep. Name the single points of failure and how you'd remove them (replication, multiple LBs), then the trade-offs you accepted.
What actually gets you the offer: thinking out loud (they're hiring your reasoning), stating trade-offs explicitly ("I'll use cache-aside, accepting brief staleness for big latency wins"), and driving the conversation rather than waiting to be asked. There's no single correct architecture — a well-justified design always beats a "perfect" one you can't explain.