Backend Field Manual
ONE-DAY CRAM · BACKEND ENGINEER, IONIC WEALTH

Backend fundamentals, field-manual style

Everything on the JD checklist — REST, auth, SQL & NoSQL, caching, scaling, Node internals — plus the scenario questions interviewers use to see if you've actually operated a system, not just read about one. Definition first, then the "why", then a concrete example.

Suggested run order for today

Morning · Parts I–IIHTTP, REST, auth & JWT — this is what your resume (WebSocket orchestration, JWT-secured APIs) already backs up. Fastest wins.
Midday · Parts III–IVSQL depth (index, transactions, joins, locking) — your PostgreSQL experience at Quantegies. Then NoSQL basics for breadth.
Afternoon · Parts V–VICaching, scaling, load balancers, then Node's event loop — ties directly to "what happens when a request hits your backend."
Evening · Part VII + cheat sheetTalk through every scenario question out loud once, unscripted. Skim the cheat sheet right before the call.
PART I

HTTP & REST fundamentals

The vocabulary every other answer today builds on. If you can narrate this section from memory, you can improvise through almost anything else.

§1.1

What happens when a request hits your backend

Definition: The end-to-end path a request takes from a user's click to a rendered response — DNS resolution, network transport, load balancing, application logic, data access, and the return trip.

This is one of the most common "walk me through it" openers. Interviewers use it to check whether you understand the system your code runs inside, not just the code itself.

Browser DNS resolve host TLS + TCP handshake Load balancer App server route → handler DB / cache query JSON response travels straight back down the same path
One round trip, six hops. Every hop is a place things can slow down or fail — which is exactly what "debug a slow API" questions probe.
  1. DNS resolution — the domain (api.ionicwealth.com) is resolved to an IP, usually cached by the OS/browser/resolver.
  2. TCP + TLS handshake — a connection is opened and, for HTTPS, encrypted (certificate exchange, key negotiation). HTTP/2 and keep-alive let this be reused across requests instead of paying this cost every time.
  3. Load balancer — sits in front of your app servers, picks one instance (round-robin, least-connections, etc.), and can also terminate TLS.
  4. Application server — the framework's router matches method + path to a handler, runs middleware (auth, logging, body parsing, validation), then your business logic.
  5. Data layer — the handler talks to a database, cache, or another internal service to do the actual work.
  6. Response — status code + headers + body are serialized and sent back down the same connection.
🎯 Interview angle
Whoever asks this is usually setting up a follow-up: "where would you add caching?", "where would you add auth?", "what if the load balancer sends you to a server that's still starting up?" Answer the base flow in under 45 seconds so you have time for the follow-up.
§1.2

What makes an API "RESTful"

Definition: REST (Representational State Transfer) is an architectural style for APIs built around resources (nouns, like /orders) manipulated through a small, uniform set of HTTP methods (verbs).

Five properties define a REST API in practice:

  • Resource-based URLs/users/42/orders, not /getUserOrders?id=42. The URL identifies a "thing", the method says what to do to it.
  • Statelessness — every request carries everything the server needs to understand it (auth token, params). The server keeps no per-client session in memory between requests — this is exactly what lets you run 10 identical app server instances behind a load balancer.
  • Uniform interface — the same verbs (GET/POST/PUT/PATCH/DELETE) mean the same thing on every resource.
  • Client–server separation — frontend and backend evolve independently as long as the contract (the API shape) holds.
  • Cacheable — responses declare whether they can be cached (via headers like Cache-Control), so intermediaries can skip round trips.
Example — resource naming
Good (resource-based)Avoid (RPC-style)
GET /orders/91GET /getOrder?id=91
POST /ordersPOST /createOrder
DELETE /orders/91POST /deleteOrder?id=91
Good to know
Nobody's API is 100% "pure REST" (true REST also implies HATEOAS — responses containing links to related actions — which almost no real API does). In interviews, "RESTful" is understood loosely as "resource-oriented HTTP API with sane verbs and status codes." Say that if asked to define it precisely.
§1.3

GET vs POST vs PUT vs PATCH vs DELETE

Definition: HTTP methods (verbs) tell the server what kind of action to perform on the resource named by the URL.
MethodPurposeBody?Safe?Idempotent?
GETRead a resourceNoYesYes
POSTCreate a resource / trigger an actionYesNoNo
PUTReplace a resource entirelyYesNoYes
PATCHPartially update a resourceYesNoUsually, but not guaranteed
DELETERemove a resourceRarelyNoYes

Safe means the method doesn't change server state — a GET should never have side effects (that's also why browsers/crawlers/proxies feel free to re-issue GETs). Idempotent means calling it once has the same end-state as calling it 5 times — covered in depth next, because it's asked constantly on its own.

The classic PUT vs PATCH mix-up: PUT /users/42 with a partial body should, strictly, wipe out any fields you didn't send (it's a full replace). PATCH /users/42 with {"name": "Yash"} changes only name and leaves everything else untouched.

Example
// PUT — full replace: any field left out is gone/reset
PUT /users/42
{ "name": "Yash", "email": "y@x.com", "role": "admin" }

// PATCH — partial update: only touches what you send
PATCH /users/42
{ "role": "editor" }
§1.4

Idempotency

Definition: An operation is idempotent if performing it multiple times produces the same result as performing it once — the end state doesn't change on repeat calls, even if the response does.

This matters because networks are unreliable. If a client sends a request and the connection drops before the response arrives, the client doesn't know if it succeeded — so it retries. If the operation isn't idempotent, that retry can double-charge a card, create two orders, or send two emails.

Naturally idempotent
GET (just reads), PUT (sets a resource to an exact state — setting it twice = setting it once), DELETE (deleting an already-deleted resource is still "gone").
Not idempotent by default
POST — "create a new order" called twice creates two orders. This is the dangerous one, and the one interviewers push on.

The fix: idempotency keys. The client generates a unique key (a UUID) per logical operation and sends it in a header. The server stores "key → result" the first time, and on any retry with the same key, returns the stored result instead of re-executing.

Example — idempotency key for a payment POST
POST /payments
Idempotency-Key: 6c1f6f2e-91ab-4e3a-9b7d-3a2e1d0c9f88
{ "amount": 4999, "currency": "INR" }

// server pseudocode
if (store.has(idempotencyKey)) {
  return store.get(idempotencyKey);   // same response, no double charge
}
const result = await charge(amount, currency);
store.set(idempotencyKey, result, { ttl: '24h' });
return result;
🎯 Interview angle
Stripe's payments API is the textbook real-world example — mention it. Also know: idempotency keys are usually scoped per-endpoint and time-boxed (store them for 24h, not forever), and the safest place to store them is the same DB transaction as the write, so a crash mid-request can't leave you in a half-idempotent state.
§1.5

HTTP status codes

Definition: A 3-digit code in the response's status line telling the client, at a glance, the outcome class of the request.
RangeMeaningCommon codes
2xxSuccess200 OK   201 Created   204 No Content
3xxRedirection301 Moved permanently   304 Not modified (cache hit)
4xxClient error — you did something wrong400 Bad request   401 Unauthenticated   403 Forbidden   404 Not found   409 Conflict   422 Unprocessable entity   429 Too many requests
5xxServer error — we did something wrong500 Internal error   502 Bad gateway   503 Service unavailable   504 Gateway timeout

The one pair that trips people up: 401 Unauthorized vs 403 Forbidden. 401 really means "unauthenticated" — you have no valid identity (missing/expired token). 403 means "I know who you are, and you're not allowed to do this." Say it exactly like that; it's a favorite gotcha.

Also worth having ready: 409 Conflict (the request clashes with current state — e.g. two updates racing, or a duplicate unique key) and 422 Unprocessable Entity (syntactically valid JSON, but semantically invalid — e.g. an email field that isn't an email).

§1.6

CORS (Cross-Origin Resource Sharing)

Definition: A browser security mechanism that blocks a web page from one origin (domain+scheme+port) from making requests to a server on a different origin — unless that server explicitly allows it via response headers.

CORS is enforced by the browser, not the server — it exists to protect users, not APIs. Without it, any malicious site you visit could silently use your logged-in cookies to call your bank's API from JavaScript running in your tab.

app.yourfrontend.com api.ionicwealth.com ① preflight: OPTIONS /orders ② 204 + Access-Control-Allow-Origin ③ actual request: POST /orders (only if ② allowed it) ④ response — browser hands it to JS, or blocks it silently
The preflight only fires for "non-simple" requests — custom headers, JSON bodies, or methods beyond GET/POST with plain form encoding.
Server-side fix (Express/Node)
const cors = require('cors');
app.use(cors({
  origin: 'https://app.yourfrontend.com',   // never use '*' if you send cookies/auth
  methods: ['GET','POST','PATCH','DELETE'],
  credentials: true
}));
Common gotcha
A CORS error in the browser console does not mean the request never reached your server — for simple requests, or even for the "actual" request after a failed preflight, the server may have processed it fully. It only means the browser withheld the response from your JavaScript. Don't debug it like a network failure.
§1.7

API design practices

Definition: The conventions that make an API predictable to consume: consistent naming, pagination for large lists, versioning so you can change the contract safely, and filtering/sorting patterns.

Versioning — put it in the URL (/v1/orders) or a header (Accept: application/vnd.ionic.v2+json). URL versioning is more common and easier to debug/cache; header versioning is "more correct" REST but harder to explore in a browser.

Pagination — never return an unbounded list.

  • Offset-based: ?page=3&limit=20. Simple, but slow on large tables (the DB still has to skip all prior rows) and unstable if rows are inserted mid-scroll.
  • Cursor-based: ?after=order_88231&limit=20. Uses an indexed column (often the ID or timestamp) as a bookmark — O(1) instead of scanning, and stable under concurrent writes. What most fintech/feed APIs use in practice.

Filtering & sorting: query params — GET /orders?status=filled&sort=-created_at.

Consistent envelope: a predictable response shape, e.g. { "data": ..., "meta": { "next_cursor": ... } } for lists, and a consistent error shape (see input validation below) for failures.

§1.8

Rate limiting

Definition: Capping how many requests a client (per API key, user, or IP) can make in a time window, to protect the backend from overload and abuse.

Four algorithms come up repeatedly:

AlgorithmHow it worksTrade-off
Fixed windowCount requests per clock window (e.g. per minute); reset at boundarySimple, but allows a burst of 2× the limit right at the window edge
Sliding windowWeighs the previous window's count into the current oneSmooths the edge-burst problem, still cheap
Token bucketBucket refills at a fixed rate; each request consumes a token; empty bucket = rejectAllows short bursts up to bucket size, industry default (AWS, Stripe use variants)
Leaky bucketRequests queue and are processed at a constant output rateSmooths traffic to a steady rate, adds latency under burst
Example — token bucket in Redis (atomic via a single command)
// pseudocode: allow 100 requests / 60s per user
key = `ratelimit:${userId}`
count = redis.INCR(key)
if (count == 1) redis.EXPIRE(key, 60)
if (count > 100) return res.status(429).send('Too Many Requests')
🎯 Interview angle
Say where you'd put it, not just how it works: at the API gateway/load-balancer layer for coarse IP-based limits, and in application middleware (backed by Redis, since it must be shared across instances — an in-memory counter per server doesn't work once you scale horizontally) for per-user limits.
§1.9

Input validation

Definition: Rejecting malformed or unexpected request data before it reaches business logic — type checks, required fields, ranges, formats.

Validate at the edge (middleware, before the handler runs), using a schema library rather than hand-rolled if chains — it's declarative, reusable, and gives you consistent error responses for free.

Example — Zod (Node.js)
const schema = z.object({
  email: z.string().email(),
  amount: z.number().positive().max(1000000),
  currency: z.enum(['INR','USD'])
});

app.post('/payments', (req, res) => {
  const parsed = schema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(422).json({ error: 'validation_failed', details: parsed.error.issues });
  }
  // parsed.data is now type-safe and trusted
});
Why it's also a security control
Input validation is your first line of defense against injection attacks (SQL injection, NoSQL injection) and against resource-exhaustion (an amount field with no upper bound, an array field with no length cap). Never trust that the frontend already validated it — a request can always be crafted by hand.

PART II

Security & authentication

Ionic Wealth handles HNI financial data — expect this part to get more follow-up questions than any other section.

§2.1

Authentication vs authorization

Definition: Authentication (AuthN) proves who you are. Authorization (AuthZ) decides what you're allowed to do. AuthN always happens first — you can't check permissions for an identity you haven't verified.
One line to remember it by
Login is authentication. "Can a Viewer role delete a client's portfolio?" is authorization.

In a typical backend, authentication is a middleware that verifies a token/session and attaches req.user. Authorization is a second, separate check — often role-based (RBAC: admin/editor/viewer) or permission-based — run per-route or per-action, using that req.user.

Example — layered middleware
router.delete('/clients/:id/portfolio',
  authenticate,                 // 401 if no valid token → sets req.user
  authorize('portfolio:delete'),  // 403 if req.user lacks this permission
  deletePortfolioHandler
);
§2.2

How JWT authentication works

Definition: A JSON Web Token is a self-contained, signed credential — the server issues it once, and can verify it on every later request without a database lookup or stored session.
HEADER alg, typ PAYLOAD sub, role, iat, exp — NOT secret SIGNATURE HMAC(header+payload, secret) . .
A JWT is just three base64url segments joined by dots: header.payload.signature. Anyone can decode and read the payload — never put secrets in it.
End-to-end flow
  1. Client sends credentials to POST /login.
  2. Server verifies the password, then signs a JWT with a server-only secret (or private key, for asymmetric signing) containing claims like { sub: userId, role, exp }.
  3. Client stores the token and sends it on every request: Authorization: Bearer <token>.
  4. Server's auth middleware re-computes the signature from the header+payload using its secret and compares it to the token's signature — no DB hit needed. If it matches and exp hasn't passed, the request is authenticated.
Example — sign & verify (Node.js, jsonwebtoken)
const token = jwt.sign(
  { sub: user.id, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: '15m' }
);

// middleware, on every protected route
const payload = jwt.verify(token, process.env.JWT_SECRET); // throws if tampered/expired
req.user = payload;
Common gotchas interviewers probe
  • JWTs can't be "revoked" the way a session can — once issued, it's valid until it expires. This is why access tokens are kept short-lived (minutes), not because of paranoia.
  • The payload is signed, not encrypted — never put a password or card number in it.
  • alg: none is a real historical vulnerability — always pin the expected algorithm when verifying, don't trust the token's own header to say how to verify it.
§2.3

Access token vs refresh token

Definition: A short-lived access token is sent on every API call. A long-lived refresh token is used only to mint new access tokens, so the user isn't forced to re-login every 15 minutes.
time → access token ✓ expired (15m) send refresh token → /token/refresh new access token refresh token itself lives ~7–30 days, often rotated on each use
Access tokenRefresh token
LifetimeMinutes (e.g. 15m)Days–weeks
SentEvery API requestOnly to the token-refresh endpoint
Stored whereMemory / short-lived cookiehttpOnly, Secure cookie (not accessible to JS — mitigates XSS theft)
If stolenDamage window is small (expires fast)Higher risk — mitigated by rotation + revocation list
🎯 Refresh token rotation
Best practice: every time a refresh token is used, issue a new refresh token and invalidate the old one, storing the current valid one (or its hash) server-side. If an old, already-rotated refresh token is ever presented, that's a signal of theft — revoke the whole token family immediately.
§2.4

How should passwords be stored

Definition: Never store passwords in plaintext or with reversible encryption. Store a salted hash produced by a slow, purpose-built algorithm — bcrypt, scrypt, or argon2 — never a fast general-purpose hash like MD5/SHA-256.

Why not SHA-256? It's designed to be fast — great for checksums, terrible for passwords, because it lets an attacker who steals your DB try billions of guesses per second on cheap GPU hardware. Bcrypt/argon2 are deliberately slow and tunable (a "cost factor"), so cracking scales in your favor as hardware improves — you just raise the cost.

Salting: a random value added per-password before hashing, stored alongside the hash. It defeats rainbow tables (precomputed hash lookup tables) and ensures two users with the same password get different hashes. Bcrypt generates and embeds the salt for you automatically.

"hunter2" + random salt bcrypt(cost=12) store this →
Example — Node.js (bcrypt)
// sign-up: hash before storing
const hash = await bcrypt.hash(plainPassword, 12);  // 12 = cost factor
await db.users.insert({ email, passwordHash: hash });

// login: compare, never decrypt
const ok = await bcrypt.compare(plainPassword, user.passwordHash);
if (!ok) return res.status(401).send('Invalid credentials');
🎯 Full checklist to recite
Hash with bcrypt/argon2, unique salt per user (handled automatically), rate-limit login attempts, respond identically whether the email or the password was wrong ("invalid credentials" — never "no such user", which leaks account existence), enforce HTTPS so the password isn't sniffed in transit, and never log the raw password anywhere.
§2.5

SQL injection

Definition: An attack where untrusted input is concatenated directly into a SQL query string, letting an attacker change the query's logic.
✗ Vulnerable
const q = `SELECT * FROM users WHERE email='${email}'`;
db.query(q);
✓ Safe — parameterized
db.query(
  'SELECT * FROM users WHERE email=$1',
  [email]
);

If email is set to ' OR '1'='1, the vulnerable query becomes WHERE email='' OR '1'='1' — always true, returning every row (or worse, chained with ; DROP TABLE users;--).

The fix is parameterized queries / prepared statements — the driver sends the SQL template and the values separately, so the database never interprets user data as SQL syntax. It's not "escaping smartly," it's a fundamentally different wire protocol.

Defense in depth (say all of these, not just one):

  • Parameterized queries / an ORM that does this for you by default (Prisma, TypeORM, SQLAlchemy).
  • Least-privilege DB users — the app's DB role shouldn't be able to DROP TABLE.
  • Input validation as an earlier layer of defense (reject obviously malformed input before it's near a query).
  • The same class of bug exists for NoSQL (NoSQL injection — e.g. passing a MongoDB operator object like { "$gt": "" } in a field expected to be a string) and shell commands (command injection) — same root cause, same fix: never let raw user input become executable syntax.
§2.6

XSS & CSRF — not in your list, but a near-certain follow-up after SQL injection

XSS (Cross-Site Scripting): attacker-controlled script runs in another user's browser, usually via unescaped user input rendered as HTML. Fix: escape/encode output, set a Content-Security-Policy header, mark auth cookies httpOnly so JS can't read them even if XSS occurs.
CSRF (Cross-Site Request Forgery): a malicious site tricks a logged-in user's browser into firing a state-changing request to your API, riding on their existing cookies. Fix: CSRF tokens, SameSite=Lax/Strict cookies, and requiring custom headers (which cross-site forms can't set) for state-changing requests.
Why this pairs with SQL injection in interviews
All three (SQLi, XSS, CSRF) are "untrusted input treated as trusted" bugs in different layers — DB, browser DOM, and browser request-credentials respectively. Naming that pattern yourself is a strong signal.
§2.7

Scenario — how would you design a login API

"Design a login API." Walk through it end to end.
  1. Endpoint & input: POST /auth/login with { email, password }, validated by schema (§1.9) before anything else touches it.
  2. Rate limit first — cap login attempts per IP and per account (e.g. 5/min) to blunt brute force and credential-stuffing, before you even query the DB.
  3. Look up the user by email (indexed column). If not found, don't reveal that — proceed to a dummy bcrypt compare anyway so response timing doesn't leak whether the email exists (timing attack defense), then return a generic 401.
  4. Verify password with bcrypt.compare against the stored hash.
  5. Issue tokens: a short-lived JWT access token + a long-lived refresh token (rotated, stored httpOnly/Secure/SameSite).
  6. Log the auth event (success/failure, IP, user-agent) — needed for audit trails and anomaly detection, especially non-negotiable at a wealth-management company.
  7. Return the access token (and user profile minus sensitive fields) with 200; set the refresh token as an httpOnly cookie rather than in the JSON body.
🎯 Depth signals interviewers look for here
Mentioning MFA/2FA as a next layer, account lockout after N failures (with unlock via email, not permanent), and — since this is a financial platform — step-up authentication for sensitive actions (re-verify identity before, say, a large withdrawal) will separate you from a "textbook" answer.

PART III

SQL databases

Direct overlap with your PostgreSQL work at Quantegies — this is where you should sound the most senior, so lean on real examples from your own job when you answer.

§3.1

SQL vs NoSQL — when to choose which

Definition: SQL (relational) databases enforce a fixed schema and strong relationships via tables + joins. NoSQL databases trade some of that structure/consistency for flexible schemas and easier horizontal scaling.
SQL (Postgres, MySQL)NoSQL (MongoDB, DynamoDB)
SchemaFixed, enforced at write timeFlexible / schema-less
RelationshipsFirst-class — joins across tablesDenormalized / embedded; joins are awkward or app-side
ConsistencyStrong (ACID transactions)Often eventual consistency at scale (tunable in some)
ScalingPrimarily vertical, or sharded with effortBuilt for horizontal scaling / sharding
Best forMoney, inventory, anything needing correctness and relationshipsHigh write-throughput logs, catalogs, flexible/evolving data, huge scale

The honest interview answer is never "NoSQL is faster" in the abstract — it's about what invariant you need. Money movement needs ACID transactions — that's a hard requirement for SQL. A high-volume, loosely-structured event log or a product catalog with wildly varying attributes per item tolerates a looser model, and gains write scalability from it.

Grounded in your own resume
Your MBO/TBBO/OHLCV market-data ingestion pipeline is a good real answer here: high-volume, append-mostly time-series data is exactly the profile that favors a wide-column/NoSQL or time-series store (or a columnar one like ClickHouse — see the JD's stack) over a heavily-normalized relational schema, while the job-lifecycle tracking sitting in PostgreSQL needs the transactional guarantees SQL gives you.
Good to know for this JD
Ionic Wealth's stack lists OpenSearch (full-text/log search, not your source of truth) and ClickHouse (columnar, built for fast aggregate queries over huge analytical datasets — think "sum trades per client per day across billions of rows") alongside Postgres/Redis. The pattern: Postgres for the transactional source of truth, ClickHouse/OpenSearch as specialized read-side stores fed from it.
§3.2

Database indexes

Definition: A separate, ordered data structure (almost always a B-tree) that lets the database jump straight to matching rows instead of scanning the whole table.
no index — full scan checks every row → O(n) indexed — B-tree lookup M A–L N–Z row ptr a few hops → O(log n)
Same query, two access paths. The index costs extra disk space and slows down writes (it must be updated too) — that's the trade-off.

Indexes speed up WHERE, JOIN ON, and ORDER BY on the indexed column(s) — but every INSERT/UPDATE/DELETE now also has to update the index, so over-indexing hurts write throughput. Index columns that are frequently filtered/joined on and selective (many distinct values) — indexing a boolean is_active column rarely helps much.

Composite indexes cover multiple columns, but column order matters: an index on (user_id, created_at) speeds up queries filtering on user_id alone or on both, but not on created_at alone (left-prefix rule).

Example
CREATE INDEX idx_orders_user_created
  ON orders (user_id, created_at);

-- uses the index (left-prefix match)
SELECT * FROM orders WHERE user_id = 42 ORDER BY created_at DESC;

-- does NOT use it efficiently — created_at alone skips the prefix
SELECT * FROM orders WHERE created_at > '2026-01-01';
🎯 Interview angle
If asked "your API is slow, what's the first thing you check?", "is there an index on the columns in the WHERE/JOIN clause, and does EXPLAIN confirm it's actually being used?" is the single highest-value sentence you can say.
§3.3

Transactions

Definition: A group of database operations that execute as a single all-or-nothing unit — either every statement commits, or (on any failure) all of them roll back, leaving no partial state.
BEGIN debit account A credit account B COMMIT ROLLBACK
If the credit fails, the debit is undone too — the classic bank-transfer example.
Example — Postgres
BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE id = 'A';
UPDATE accounts SET balance = balance + 500 WHERE id = 'B';
COMMIT;   -- or ROLLBACK if either UPDATE throws
// Node.js, node-postgres — must use the SAME client for all 3 calls
const client = await pool.connect();
try {
  await client.query('BEGIN');
  await client.query('UPDATE accounts SET balance = balance - $1 WHERE id=$2', [500,'A']);
  await client.query('UPDATE accounts SET balance = balance + $1 WHERE id=$2', [500,'B']);
  await client.query('COMMIT');
} catch (e) {
  await client.query('ROLLBACK');
  throw e;
} finally {
  client.release();
}
§3.4

ACID properties

Definition: The four guarantees a transaction gives you — the reason SQL databases are trusted with money.
Atomicity
All-or-nothing — partial writes are impossible, even on a crash mid-transaction.
Consistency
A transaction takes the DB from one valid state to another — constraints, foreign keys, and triggers are never violated, even transiently as seen from outside.
Isolation
Concurrent transactions don't see each other's uncommitted intermediate state. Controlled by isolation levels (Read Committed, Repeatable Read, Serializable) — stronger isolation costs more throughput.
Durability
Once committed, it survives a crash/power loss — written to disk (via a write-ahead log), not just memory.
🎯 If asked "why does this matter for fintech"
Say it directly: a trade or a balance update can never be allowed to half-apply. ACID is what stops "money left account A but never reached account B" from being a possible outcome of a crash.
§3.5

INNER JOIN vs LEFT JOIN

Definition: Joins combine rows from two tables based on a related column. INNER JOIN keeps only rows with a match in both tables. LEFT JOIN keeps every row from the left table, filling in NULLs where there's no match on the right.
INNER JOIN users orders only matching rows LEFT JOIN users orders all left + matches (NULLs ok) FULL JOIN users orders everything, matched where possible
Example
-- INNER JOIN: only users who have placed at least one order
SELECT u.name, o.id FROM users u
INNER JOIN orders o ON o.user_id = u.id;

-- LEFT JOIN: every user, orders.id is NULL if they've never ordered
SELECT u.name, o.id FROM users u
LEFT JOIN orders o ON o.user_id = u.id;

RIGHT JOIN is the mirror of LEFT (rarely used — people just swap table order and use LEFT). FULL OUTER JOIN keeps unmatched rows from both sides. A good one-liner if asked "when would you use LEFT over INNER": "whenever the absence of a match is itself meaningful information" — e.g. "find users who have never placed an order" is a LEFT JOIN with WHERE o.id IS NULL.

§3.6

Connection pooling

Definition: A pre-opened, reused set of database connections shared across incoming requests, instead of opening (and TLS-handshaking) a brand-new connection per request.
requests req A req B req C pool (max: 10) conn 1 busy conn 2 busy conn 3 idle ... idle req C waits in queue for a free conn PostgreSQL
Postgres can only hold so many concurrent connections (each is a real OS process) — an unbounded pool per app instance, times N instances, can itself take the DB down.

Opening a DB connection is expensive (TCP + auth handshake). Without pooling, every request pays that cost. A pool keeps, say, 10 warm connections open and hands them out and back — a request borrows one, runs its query, returns it.

Sizing it isn't "bigger is always better." Postgres has a hard max_connections limit (default 100), shared across every app instance talking to it. If you run 20 app instances × pool size 20, that's 400 potential connections against a DB that allows 100 — connections start getting refused. This is exactly why tools like PgBouncer exist: a proxy that pools connections at the infrastructure level, in front of many app instances.

Example — Node.js (pg)
const pool = new Pool({ max: 10, idleTimeoutMillis: 30000 });
const result = await pool.query('SELECT * FROM orders WHERE id=$1', [id]);
// connection is auto-returned to the pool after the query
§3.7

Normalization vs denormalization — bonus, pairs naturally with joins/indexes

Definition: Normalization splits data into linked tables to remove duplication (each fact stored once). Denormalization deliberately duplicates data to avoid joins and speed up reads.

A normalized schema stores a user's address once in a users table and references it by user_id everywhere; a denormalized one might copy the shipping address onto every order row so reading an order never needs a join — at the cost of updating N rows if the address changes, and the risk of them drifting out of sync. OLTP systems (transactional, like Postgres for orders) lean normalized; read-heavy analytics/reporting systems lean denormalized on purpose.

§3.8

The N+1 query problem & EXPLAIN — bonus, this is the real answer to "how would you find a slow query"

Definition: A pattern where fetching a list (1 query) then loops over it fetching related data per item (N more queries) — instead of 1 query, you run N+1.
✗ N+1
const orders = await db.query('SELECT * FROM orders');
for (const o of orders) {
  o.user = await db.query(
    'SELECT * FROM users WHERE id=$1',[o.user_id]);
}
✓ One query, joined/batched
SELECT o.*, u.name FROM orders o
JOIN users u ON u.id = o.user_id;
-- or DataLoader-style batching by user_id IN (...)

To actually diagnose a slow query, run EXPLAIN ANALYZE — it shows whether Postgres used an index (Index Scan) or scanned every row (Seq Scan), plus real vs. estimated row counts and time per step. This is the concrete, checkable version of "check for missing indexes."

§3.9

Optimistic vs pessimistic locking

Definition: Two strategies for handling concurrent writes to the same row. Pessimistic locking blocks other writers up front. Optimistic locking lets everyone proceed and checks for conflicts at write time.
Pessimistic — lock, then edit
BEGIN;
SELECT * FROM accounts
  WHERE id=1 FOR UPDATE; -- locks row
-- other transactions trying to
-- touch this row now wait
UPDATE accounts SET balance=... WHERE id=1;
COMMIT;
Good when conflicts are likely/costly (financial balances).
Optimistic — version, then check
UPDATE accounts
SET balance = 4500, version = version + 1
WHERE id = 1 AND version = 7;
-- 0 rows affected → someone else
-- updated it first → retry/reject
Good when conflicts are rare (profile edits) — no lock, higher throughput.
🎯 This is the mechanism behind the "two users edit the same resource" scenario question below
Jump to §7.3 for the full scenario answer.

PART IV

NoSQL & MongoDB

Likely lighter than Part III in this interview given the JD's stack — but "when to embed vs reference" is a near-guaranteed question if MongoDB comes up at all.

§4.1

Embedding vs referencing (MongoDB)

Definition: Embedding nests related data inside the same document. Referencing stores related data in a separate collection and links it by ID — MongoDB's version of a foreign key, without enforced joins.
Embedded
{
  _id: "order_1",
  user: "Yash",
  items: [
    { sku: "A1", qty: 2 },
    { sku: "B4", qty: 1 }
  ]
}
Referenced
{ _id: "order_1",
  user_id: "u_88",
  item_ids: ["it_1","it_2"] }

{ _id: "u_88", name: "Yash" }
{ _id: "it_1", sku: "A1" }
Embed whenReference when
Data is always read together ("give me this order with its items")Related data is large, grows unbounded, or is shared/reused across many parents
The nested data belongs to, and lives/dies with, the parent (order items)The related entity is updated independently and often (a user's profile referenced from thousands of orders)
One read gets you everything — no extra round tripAvoids duplicating (and having to keep in sync) the same data everywhere it's referenced
The classic trap
Embedding a user's full profile inside every order looks fast to read, but now a user changing their name means updating every order document that embedded it — and MongoDB documents have a 16MB size cap, so unbounded embedded arrays (e.g. embedding every comment ever on a post) eventually break outright. Rule of thumb: embed one-to-few, reference one-to-many/many-to-many.
§4.2

MongoDB aggregation pipeline

Definition: A sequence of stages that transform documents step by step — MongoDB's equivalent of SQL's GROUP BY / JOIN / HAVING, expressed as a pipeline instead of one declarative statement.
$match $group $sort $project $limit
Each stage's output feeds the next — filter early (cheap), transform/shape late.
Example — total order value per user, top 5
db.orders.aggregate([
  { $match: { status: "completed" } },              // filter first — cheap
  { $group: { _id: "$user_id", total: { $sum: "$amount" } } },
  { $sort: { total: -1 } },
  { $limit: 5 }
]);

$lookup is the aggregation stage that performs a left-outer-join-style merge against another collection — the closest MongoDB gets to a SQL JOIN, and worth naming if asked "does MongoDB support joins."

§4.3

CAP theorem — bonus, underlies every SQL-vs-NoSQL trade-off answer

Definition: In a distributed system, when a network partition happens, you can only keep Consistency (every node sees the same data) or Availability (every request gets a response) — not both. Partition tolerance (P) isn't optional in a real distributed system, so the real trade-off in practice is CP vs AP.

Postgres (single primary) is effectively CP-leaning for writes. Many NoSQL stores (Cassandra, DynamoDB) default to AP — favoring "always answer, maybe slightly stale" over refusing to answer. This is why "eventual consistency" shows up so often in the NoSQL column of §3.1's table — it's a direct consequence of choosing availability.


PART V

Performance & scale

This part is where "how to improve a slow API" and "sudden traffic spike" get their building blocks — read it right before Part VII.

§5.1

Caching

Definition: Storing a copy of expensive-to-compute or slow-to-fetch data somewhere faster (usually memory — Redis) so repeat reads skip the expensive path.
client app server ① check cache Redis hit → return (fast, ~ms) ② miss → query DB, then write result back to Redis Postgres
Cache-aside (a.k.a. lazy loading) — the most common pattern in practice.
StrategyHowTrade-off
Cache-asideApp checks cache first; on miss, reads DB and populates cacheSimple, most common; first request after expiry is always slow
Write-throughEvery write goes to cache and DB togetherCache always fresh; adds latency to writes
Write-backWrite hits cache immediately, DB is updated asynchronously laterFastest writes; risk of data loss if cache dies before flush

Invalidation is the hard part ("there are only two hard things in computer science: cache invalidation and naming things"). Two common approaches: a TTL (expire after N seconds — simple, tolerates some staleness) and explicit invalidation (delete/update the cache key the moment the underlying data changes — fresher, but easy to miss a code path and leave stale data).

What to cache: read-heavy, rarely-changing, expensive-to-compute data — a user's permission set, a computed dashboard aggregate, a market-data snapshot. Not: data that must always be perfectly fresh for correctness (an account balance mid-transaction).

Example — cache-aside in Node.js
async function getUser(id) {
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);        // cache hit

  const user = await db.users.findById(id);        // cache miss
  await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 300); // TTL 5m
  return user;
}
§5.2

Load balancers

Definition: A layer that sits in front of multiple app server instances and distributes incoming requests across them, so no single instance is overwhelmed and the app can scale horizontally.
clients loadbalancer server 1 server 2 server 3 also health-checks each server, pulling dead ones out of rotation

Common distribution strategies: round-robin (cycle through servers), least connections (send to whichever has the fewest active requests — better when requests vary in cost), IP hash (same client always hits the same server — useful for in-memory session affinity, though that itself conflicts with the "stateless" REST principle from §1.2, which is exactly why sessions belong in Redis, not server memory).

Also a health-check mechanism: it pings each instance and stops routing to ones that fail, which is how a rolling deploy or a crashed instance doesn't take the whole service down.

§5.3

Horizontal vs vertical scaling — bonus

Vertical: bigger machine — more CPU/RAM on the same box. Simple, no code changes, but has a hard ceiling and a single point of failure.
Horizontal: more machines, load-balanced. Scales further, adds redundancy — but requires the app to be stateless (§1.2) and the database to handle more concurrent connections (§3.6).

Stateless application servers scale horizontally easily. The database is usually the harder part to scale horizontally — which is exactly why replication and sharding (next) exist.

§5.4

Sharding & replication — bonus

Replication: copies of the same data on multiple DB nodes — one primary (handles writes), one or more replicas (handle reads). Scales read throughput and adds durability; replicas can lag slightly behind the primary (replication lag → a classic source of "I just wrote this, why can't I read it back" bugs).
Sharding: splitting data across nodes by a shard key (e.g. user_id % 4), so each node holds a different subset. Scales both read and write throughput, but cross-shard queries/joins get expensive and picking a good shard key is genuinely hard to change later.
§5.5

Message queues — Kafka / SQS — bonus, listed as "good to have" on the JD

Definition: A middleman that lets one service publish a message and another consume it asynchronously, decoupling the two — the producer doesn't wait for the consumer to finish.

Use it when a request shouldn't have to wait on slow/non-critical work: sending an email, generating a report, updating a search index. The API handler publishes a message and returns immediately; a worker processes it separately, and can retry on failure without the user ever seeing an error.

Kafka is a distributed log — many consumers can independently read the same stream at their own pace, messages persist for a configurable time, great for high-throughput event streaming (fits Ionic Wealth's market-data-adjacent domain). SQS is a simpler point-to-point queue — a message is typically consumed once and removed, good for task/job queues.

Example — decoupling a slow step
// instead of doing this inline and making the client wait:
app.post('/orders', async (req,res)=>{
  const order = await createOrder(req.body);
  await kafka.produce('order.created', order);  // fire and forget
  res.status(201).json(order);                  // respond immediately
});
// a separate worker consumes 'order.created' and sends the confirmation email,
// updates analytics, notifies the risk engine — independently, with its own retries
§5.6

Scenario — how would you improve a slow API

Treat this as a checklist question — name the layers in order, from cheapest/most-likely to most invasive:

LayerWhat to check / do
1. Measure firstProfile/log timings per stage — don't guess. Is it the DB, an external API, or CPU-bound app code?
2. DatabaseEXPLAIN ANALYZE the slow query, check for missing indexes (§3.2), N+1 queries (§3.8), and connection pool exhaustion (§3.6)
3. CachingIs repeated, mostly-static data being recomputed every request? Add caching (§5.1)
4. PayloadAre you over-fetching — returning fields nobody needs, or an unpaginated list (§1.7)?
5. ConcurrencyAre independent operations (e.g. two unrelated API calls) run sequentially with await when they could run in parallel with Promise.all?
6. External dependenciesIs a slow third-party API blocking the response? Add a timeout, or move it off the critical path (queue it — §5.5)
7. Scale outIf the app itself is CPU-bound and optimized, add more instances behind the load balancer (§5.2)
🎯 Interview angle
Starting with "I'd add more servers" reads as junior. Starting with "I'd measure where the time is actually going" reads as senior — say that sentence first, every time this kind of question comes up.

PART VI

Node.js internals

The JD asks for Node.js explicitly — and "what happens when a request reaches your Node backend" is really an event-loop question in disguise.

§6.1

A request inside your Node.js backend

Definition: Node.js is single-threaded for your JavaScript, but non-blocking — it never sits idle waiting on I/O, it hands that work off and moves on to the next thing until the result is ready.
Walk through GET /orders/91 hitting an Express app
  1. The OS hands the incoming connection to libuv's event notification system; Node's single JS thread is free to keep handling other requests the whole time.
  2. Express's router matches the method + path to a handler, running middleware in order (body parser, auth, validation — §2.1, §1.9).
  3. Your handler calls await db.query(...). This doesn't block the thread — Node delegates the actual socket I/O to the OS/libuv's thread pool for certain operations, registers a callback, and the JS thread is immediately free to process other incoming requests.
  4. When the DB responds, its callback is queued. The event loop picks it up on its next pass, resumes your async function exactly where it awaited.
  5. Your handler finishes, calls res.json(...), and the response is written back on that same connection.
🎯 The one sentence that answers this well
"Node handles thousands of concurrent requests on one thread because none of them block each other while waiting on I/O — the event loop switches to whatever's ready next, instead of a thread sitting idle." That line alone answers 80% of what's being tested.
§6.2

The event loop

Definition: The mechanism that lets Node run non-blocking I/O on a single JS thread — a loop that repeatedly checks queues of pending callbacks and runs whichever are ready, in a fixed phase order.
timers setTimeout / setInterval pending callbacks poll fetch new I/O events, runs I/O callbacks check setImmediate close callbacks microtasks Promises / process.nextTick drained between EVERY phase
Simplified libuv phase order. What matters for interviews: microtasks (Promise callbacks) always run before the loop moves to its next phase — that's why Promises "jump the queue" ahead of a setTimeout(fn, 0).
Example — order of execution (a real gotcha question)
console.log('1 sync');
setTimeout(()=>console.log('2 macrotask'), 0);
Promise.resolve().then(()=>console.log('3 microtask'));
console.log('4 sync');
// output: 1 sync, 4 sync, 3 microtask, 2 macrotask
// all sync code finishes → all microtasks drain → THEN the next phase's macrotasks run
The one real danger
CPU-heavy synchronous code (a huge loop, JSON.parse on a massive payload, unoptimized crypto) blocks the single thread — every other request stalls until it finishes, because there's no thread to interleave onto. This is the actual reason Node isn't great for CPU-bound work and why you'd offload it to a worker thread, a queue (§5.5), or a separate service.
§6.3

Blocking vs non-blocking I/O

Definition: Blocking I/O halts the thread until an operation (disk read, network call) completes. Non-blocking I/O starts the operation, returns control immediately, and delivers the result later via a callback/Promise/event.
Example
// blocking — freezes the whole process until the file is read
const data = fs.readFileSync('big.json');

// non-blocking — thread is free immediately, callback fires on completion
fs.readFile('big.json', (err, data) => { /* ... */ });

Almost every Node.js standard-library and DB-driver method has a non-blocking (async) form — using the sync form (anything ending in Sync) inside a request handler is a classic performance bug, because it blocks every other in-flight request on the same process while it runs.


PART VII

Scenario & system-design questions

These are the questions that actually decide the interview — they check whether you can compose everything above under pressure. Practice saying each answer out loud, once, before the call.

§7.1

An API suddenly starts receiving huge traffic. What would you do?

Structure the answer as: protect → diagnose → scale → prevent
  1. Protect first — this is happening now, so stop the bleeding before investigating: confirm rate limiting (§1.8) is active so one client can't starve everyone else, and check auto-scaling / load balancer health so unhealthy instances aren't still taking traffic.
  2. Diagnose in parallel — is this legitimate traffic (a marketing push, a viral moment) or abuse (a bot, a retry storm from a buggy client, a DDoS)? Check request patterns: one IP/key vs. broad and organic.
  3. Scale the bottleneck, not everything — check dashboards/APM: is it CPU-bound app servers (scale out horizontally, §5.3), or is the database the real limit (in which case adding app servers alone just moves the queue to the DB connection pool, §3.6)?
  4. Shed load gracefully if needed — serve cached/stale responses instead of erroring, return 503 with Retry-After for non-critical endpoints, prioritize critical paths (checkout) over non-critical ones (recommendations).
  5. Prevent recurrence — if it was legitimate growth, this becomes a capacity-planning/auto-scaling conversation. If it was abuse, tighten rate limits/WAF rules and add alerting so it's caught earlier next time.
§7.2

Your database query is slow. What do you check?

This is §3.2 + §3.8, turned into a live debugging narrative
  1. Run EXPLAIN ANALYZE on the exact query — is it doing a Seq Scan where an Index Scan should be possible?
  2. Check whether an index exists on the columns in WHERE/JOIN/ORDER BY — and if a composite index exists, whether the query actually matches its left-prefix (§3.2).
  3. Look for an N+1 pattern in the calling code, not just the one query (§3.8) — sometimes "one slow query" is really 200 fast ones.
  4. Check table statistics are current (ANALYZE) — Postgres's query planner picks a bad plan if its row-count estimates are stale.
  5. Check for lock contention — is this query waiting on a lock held by another long-running transaction (§3.9)?
  6. Check data volume — has the table grown 100x since this query was written and the index/query strategy just hasn't kept up?
  7. If it's a genuinely expensive aggregate that runs often, consider caching the result (§5.1) or moving it to a read-optimized store (ClickHouse, per this JD's stack).
§7.3

Two users update the same resource at the same time. What can happen?

Name the failure mode, then the fix

What can happen — the "lost update" problem: both read the row at version 7. User A writes their change. User B, still holding the stale version 7 in memory, writes their change right after — silently overwriting A's update with no error, no conflict, no trace that A's change ever happened.

  1. Pessimistic locking (§3.9) — SELECT ... FOR UPDATE so the second writer physically waits for the first transaction to finish. Right choice when conflicts are frequent and correctness is non-negotiable — a shared financial balance.
  2. Optimistic locking (§3.9) — add a version column; each update's WHERE clause checks the version it read. If 0 rows are affected, someone else won first — return 409 Conflict and let the client re-fetch and retry. Right choice when conflicts are rare — most profile/document edits.
  3. Application-level merge — for collaborative editing (think Google Docs), don't just reject conflicts; use operational transforms or CRDTs to merge both changes. Worth mentioning if you've built this — your NoteSync project used OT for exactly this reason.
§7.4

An external API your backend depends on is slow or unavailable. What would you do?

Four mechanisms, in the order you'd reach for them
  1. Timeouts — never call an external API without one. Without a timeout, a hung dependency hangs your request handler (and, since Node is single-threaded, potentially backs up everything behind it — §6.2).
  2. Retries with exponential backoff (+ jitter) — for transient failures, retry after 100ms, 200ms, 400ms... with a little randomness so many clients don't all retry in lockstep and hammer the dependency the moment it recovers. Only retry idempotent operations (§1.4) — retrying a non-idempotent POST blindly can duplicate the side effect.
  3. Circuit breaker — after enough consecutive failures, stop calling the dependency for a cooldown period and fail fast instead (return a cached/default response or a clear error). This protects your service from wasting threads/connections on a dependency that's already down, and gives the dependency room to recover instead of being retried into the ground by everyone at once.
  4. Fallback / degrade gracefully — serve a cached last-known-good value, queue the work for later (§5.5) if it doesn't need to be synchronous, or clearly surface a partial failure to the user instead of failing the entire request over one non-critical dependency.
CLOSEDcalls flow normally failures > threshold OPENfail fast, no calls cooldown elapses HALF-OPENtrial request
Circuit breaker state machine — CLOSED → OPEN → HALF-OPEN (trial) → back to CLOSED or OPEN.

§8

60-second cheat sheet

Skim this in the elevator, not before. Every line links back to its full section above.

IdempotencySame call, N times = same end state. GET/PUT/DELETE yes, POST no (use an idempotency key).
401 vs 403401 = who are you? 403 = I know who you are, and no.
CORSBrowser-enforced, not server-enforced. Preflight OPTIONS checks before the real request.
JWTSigned, not encrypted. Stateless verification. Short-lived access + longer refresh token.
Passwordsbcrypt/argon2, never MD5/SHA. Salted. Slow on purpose.
SQL injectionParameterized queries, never string-concatenated SQL.
IndexesB-tree, O(log n) vs O(n) scan. Speeds reads, costs writes.
ACIDAtomic, Consistent, Isolated, Durable — why transactions are trusted with money.
JOININNER = only matches. LEFT = all of the left table, NULLs where no match.
Connection poolingReuse warm DB connections instead of opening one per request.
Embed vs referenceEmbed one-to-few & read-together. Reference one-to-many & independently-updated.
Optimistic vs pessimistic lockingOptimistic = check version at write time. Pessimistic = lock row up front.
CachingCache-aside is default. TTL for simple staleness, explicit invalidation for freshness.
Load balancerDistributes requests + health-checks instances. Needs stateless app servers.
Node event loopSingle thread, non-blocking I/O. Microtasks drain before each phase. CPU-heavy sync code blocks everyone.
Slow API checklistMeasure → index/N+1 → cache → payload size → parallelize → external deps → scale out.
Dependency downTimeout → retry with backoff → circuit breaker → fallback/degrade.
Concurrent updateLost update problem. Fix with optimistic (version check) or pessimistic (row lock).
Before you get on the call
Pick three answers above where you can swap in a real detail from your own resume (BaQLabs' WebSocket reconnection handling, the MBO/TBBO ingestion pipeline, or the CI/CD pipeline) — a concrete example beats a textbook-perfect definition every time. Good luck.