Node.jsSpring BootPostgreSQLSystem DesignFAANG Prep

Backend Interview — The Complete Guide

Everything you need for backend rounds at product companies. Node.js internals, Spring Boot deep dive, database optimization, system design thinking, and the questions that actually get asked.

150 min read13 sections
01

Node.js Internals

Node.js runs JavaScript on the server using V8 and libuv. The single-threaded event loop is the core concept — understand it deeply and you'll answer 80% of Node interview questions.

🔥 Event Loop — Node vs Browser

Both use an event loop, but Node's has phases: timers → pending callbacks → idle/prepare → poll → check → close. The browser's loop is simpler: task → microtasks → render. The key difference: Node has setImmediate (check phase) and process.nextTick (runs before any phase, even before microtasks).

node-event-loop.tstypescript
// Node.js execution order
console.log("1 — sync");

setTimeout(() => console.log("2 — timer phase"), 0);
setImmediate(() => console.log("3 — check phase"));

process.nextTick(() => console.log("4 — nextTick"));
Promise.resolve().then(() => console.log("5 — microtask"));

console.log("6 — sync");

// Output: 1, 6, 4, 5, 2, 3
// nextTick > microtask > timer > immediate (from main module)
// Note: timer vs immediate order can vary inside I/O callbacks
FeatureNode.jsBrowser
Event loop6 phases (libuv)Task → microtasks → render
nextTick✅ process.nextTick (highest priority)❌ Not available
setImmediate✅ Check phase❌ Not standard
I/Olibuv thread pool (fs, dns, crypto)Web APIs (fetch, setTimeout)
ThreadsWorker threads (manual)Web Workers

Non-blocking I/O

Node delegates I/O operations (file reads, network calls, DB queries) to libuv's thread pool or OS-level async APIs. The main thread never blocks — it registers a callback and moves on. When the I/O completes, the callback is queued for the event loop. This is why Node handles thousands of concurrent connections with a single thread.

When Node IS blocking

CPU-intensive work (JSON parsing large payloads, image processing, crypto hashing) blocks the event loop because it runs on the main thread. Solutions: Worker Threads, child processes, or offload to a separate service.

Streams & Buffers

Streams process data in chunks instead of loading everything into memory. A 2GB file? Stream it — don't readFileSync it. Four types: Readable, Writable, Duplex, Transform. Buffers are fixed-size chunks of raw binary data — the building blocks streams operate on.

streams.tstypescript
import { createReadStream, createWriteStream } from "fs";
import { pipeline } from "stream/promises";
import { createGzip } from "zlib";

// Stream a file through gzip compression to output
await pipeline(
  createReadStream("input.log"),    // Readable
  createGzip(),                      // Transform
  createWriteStream("output.log.gz") // Writable
);
// Memory usage stays constant regardless of file size

Clustering & Scaling

Node.js is single-threaded, so it can only utilize one CPU core per process. The cluster module allows us to fork multiple worker processes, each running its own event loop and sharing the same server port. This helps utilize all CPU cores. However, in production, tools like PM2 or Kubernetes are preferred for better process management and scalability.

In Node.js clustering, the master (primary) process is responsible for creating and managing worker processes, while the workers handle actual incoming requests.

📝 Quick Revision

Quick Revision Cheat Sheet

Event loop: 6 phases. nextTick > microtask > timer > immediate.

Non-blocking I/O: libuv handles I/O off main thread. Callbacks queued when done.

Blocking trap: CPU work blocks the loop. Use Worker Threads for heavy computation.

Streams: Process data in chunks. Readable, Writable, Duplex, Transform.

Clustering: Fork workers per CPU core. Use PM2 or K8s in production.

Common Interview Questions

Q:How does Node.js handle concurrent requests if it's single-threaded?

A: Node delegates I/O to libuv (thread pool + OS async APIs). The main thread registers callbacks and continues processing. When I/O completes, callbacks are queued in the event loop. This lets one thread handle thousands of concurrent connections — it's not doing the I/O work itself.

Q:What's the difference between process.nextTick and setImmediate?

A: nextTick fires before any I/O event or timer in the current iteration — it's the highest priority callback. setImmediate fires in the 'check' phase, after I/O events. In practice: nextTick for 'do this before anything else', setImmediate for 'do this after current I/O'.

02

API Design

API design questions test whether you can build interfaces that are predictable, scalable, and pleasant to consume. Think like the developer who has to use your API.

REST Principles

  • Resources as nouns: /users, /orders/123 — not /getUser
  • HTTP verbs for actions: GET (read), POST (create), PUT (replace), PATCH (update), DELETE (remove)
  • Stateless: each request contains all info needed — no server-side session dependency
  • Consistent responses: always return the same shape — { data, error, meta }

🔥 Idempotency — Must Know

An idempotent operation produces the same result no matter how many times you call it. GET, PUT, DELETE are idempotent. POST is not. This matters for retries — if a network timeout occurs, can you safely retry? With idempotent endpoints, yes.
GET, PUT, and DELETE are idempotent because repeating them does not change the final state of the server, whereas POST is not idempotent since each request creates a new resource.

Interview follow-up: How do you make POST idempotent?

Use an idempotency key. The client sends a unique key (UUID) in a header. The server checks if that key was already processed. If yes, return the cached response. If no, process and store the result keyed by that UUID. Stripe uses this pattern.

Pagination Strategies

StrategyHow it worksProsCons
Offset-based?page=3&limit=20 → OFFSET 40 LIMIT 20Simple, supports jump-to-pageSlow on large offsets, inconsistent with inserts
Cursor-based?cursor=abc123&limit=20 → WHERE id > cursorFast at any depth, consistentNo jump-to-page, cursor must be opaque
Keyset?after_id=500&limit=20Like cursor but with a real columnRequires a sortable, unique column

Versioning

API versioning is the practice of managing changes to an API by introducing versions so that existing clients continue to work without breaking.

URL path versioning (/api/v1/users) is the most common and explicit. Header versioning (Accept: application/vnd.api.v2+json) is cleaner but harder to test in a browser. Pick one and be consistent. In interviews, mention that you version to avoid breaking existing clients when the API evolves.

Error Handling Standards

error-response.tstypescript
// Consistent error response shape
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Email is required",
    "details": [
      { "field": "email", "message": "must not be empty" }
    ]
  },
  "meta": { "requestId": "req_abc123", "timestamp": "2026-04-01T10:00:00Z" }
}

// Use proper HTTP status codes:
// 400 — bad input (client's fault)
// 401 — not authenticated
// 403 — not authorized
// 404 — resource not found
// 409 — conflict (duplicate)
// 422 — unprocessable entity (valid JSON but invalid data)
// 429 — rate limited
// 500 — server error (our fault)

📝 Quick Revision

Quick Revision Cheat Sheet

REST: Nouns for resources, verbs for actions, stateless, consistent responses.

Idempotency: GET/PUT/DELETE are idempotent. Make POST idempotent with idempotency keys.

Pagination: Offset for simple UIs, cursor for infinite scroll / large datasets.

Versioning: URL path (/v1/) is most common. Version to avoid breaking clients.

Errors: Consistent shape. Proper status codes. Include requestId for debugging.

03

Authentication & Authorization

Auth is asked in almost every backend interview. Know the difference between authentication (who are you?) and authorization (what can you do?), and the trade-offs between JWT and sessions.

🔥 JWT vs Sessions — Must Know

AspectJWT (Token-based)Session (Server-side)
StateStateless — token contains all claimsStateful — session stored on server (Redis/DB)
StorageClient (localStorage, cookie, memory)Server (session store) + cookie (session ID)
ScalabilityEasy — no shared state between serversHarder — need shared session store
RevocationHard — can't invalidate until expiryEasy — delete from session store
SizeLarger (payload + signature)Small cookie (just session ID)
Best forAPIs, microservices, mobile appsTraditional web apps, when revocation matters

💡 Pro Tip — The follow-up trap

After you explain JWT, the interviewer will ask: "How do you revoke a JWT?" Answer: short expiry (15 min) + refresh tokens stored server-side. On logout, invalidate the refresh token. For immediate revocation, maintain a blocklist (Redis) of revoked JWTs — but this adds statefulness, which defeats the purpose.

OAuth 2.0 Basics

OAuth lets users grant third-party apps limited access without sharing passwords. The Authorization Code flow (with PKCE) is the standard for web apps: redirect to provider → user consents → provider redirects back with a code → your server exchanges the code for tokens. Know the roles: Resource Owner (user), Client (your app), Authorization Server (Google/GitHub), Resource Server (API).

RBAC (Role-Based Access Control)

Assign roles (admin, editor, viewer) to users. Each role has a set of permissions. Check permissions at the API layer, not just the UI. Common pattern: middleware that checks req.user.role against the required permission for the endpoint.

📝 Quick Revision

Quick Revision Cheat Sheet

JWT: Stateless token. Easy to scale. Hard to revoke. Use short expiry + refresh tokens.

Sessions: Server-side state. Easy to revoke. Need shared store for multi-server.

OAuth: Delegated auth. Authorization Code + PKCE for web apps.

RBAC: Roles → permissions. Check at API layer, not just UI.

Refresh tokens: Long-lived, stored server-side. Used to get new access tokens.

04

Rate Limiting & Throttling

🔥 Very Important — Asked frequently

Rate limiting is a favorite interview topic because it combines algorithm knowledge, system design thinking, and real-world trade-offs. Know at least two algorithms and where to implement.

Why Rate Limiting?

  • Prevent abuse (brute-force login, scraping, DDoS)
  • Protect downstream services from overload
  • Ensure fair usage across clients
  • Control costs (API calls to third-party services)

🔥 Algorithms

AlgorithmHow it worksProsCons
Token BucketBucket holds N tokens. Each request consumes one. Tokens refill at a fixed rate.Allows bursts up to bucket size. Simple.Burst can overwhelm downstream briefly.
Sliding Window LogStore timestamp of each request. Count requests in the last N seconds.Precise. No boundary issues.Memory-heavy (stores every timestamp).
Sliding Window CounterCombine current + previous window counts weighted by overlap.Memory-efficient. Smooth.Approximate — not perfectly precise.
Fixed WindowCount requests per fixed time window (e.g., per minute).Simplest to implement.Boundary spike: 2x burst at window edges.
Leaky BucketRequests enter a queue. Processed at a fixed rate. Queue overflow = reject.Smooth output rate.Doesn't allow any bursts.

Where to Implement

LayerWhen to useTrade-off
API Gateway (Nginx, Kong, AWS API GW)Global rate limiting across all servicesCoarse-grained. Can't do per-user logic easily.
Application middlewarePer-user, per-endpoint, custom logicMore flexible. Adds latency. Need shared store (Redis).
Load balancerConnection-level limitingVery fast. Limited to IP-based rules.
rate-limiter-redis.tstypescript
// Sliding window counter with Redis
async function isRateLimited(userId: string, limit: number, windowSec: number): Promise<boolean> {
  const key = `rate:${userId}`;
  const now = Date.now();
  const windowStart = now - windowSec * 1000;

  // Remove old entries, add current, count
  const multi = redis.multi();
  multi.zremrangebyscore(key, 0, windowStart);  // remove expired
  multi.zadd(key, now, `${now}`);               // add current request
  multi.zcard(key);                              // count in window
  multi.expire(key, windowSec);                  // auto-cleanup

  const results = await multi.exec();
  const count = results[2][1] as number;
  return count > limit;
}

📝 Quick Revision

Quick Revision Cheat Sheet

Token Bucket: Allows bursts. Tokens refill at fixed rate. Most common in production.

Sliding Window: Precise counting. Use Redis sorted sets for distributed systems.

Fixed Window: Simplest. Boundary spike problem at window edges.

Where: API gateway for global limits. App middleware for per-user logic.

Storage: Redis for distributed rate limiting. In-memory for single-server.

05

Databases (PostgreSQL)

Database questions separate backend devs who write queries from those who understand why queries are fast or slow. PostgreSQL knowledge is highly valued at product companies.

🔥 Indexing — When and When Not

An index is a separate data structure (B-tree by default in Postgres) that speeds up lookups. Without an index, Postgres does a sequential scan — reads every row. With an index on the WHERE column, it does an index scan — jumps directly to matching rows.

Index when...Don't index when...
Column is in WHERE, JOIN, or ORDER BY frequentlyTable is small (< 1000 rows) — seq scan is faster
Column has high cardinality (many unique values)Column has low cardinality (boolean, status with 3 values)
Read-heavy workloadWrite-heavy workload (indexes slow down INSERT/UPDATE)
You need to enforce uniquenessYou're indexing every column 'just in case'
indexing.sqltypescript
-- Check if your query uses an index
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 42;

-- Create an index
CREATE INDEX idx_orders_user_id ON orders(user_id);

-- Composite index (order matters!)
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
-- Works for: WHERE user_id = 42
-- Works for: WHERE user_id = 42 AND created_at > '2026-01-01'
-- Does NOT work for: WHERE created_at > '2026-01-01' (leftmost prefix rule)

Joins vs Denormalization

ApproachWhen to useTrade-off
Normalized + JOINsData integrity matters, moderate read loadSlower reads, faster writes, no data duplication
DenormalizedRead-heavy, need sub-ms response, analyticsFaster reads, slower writes, data can go stale
Materialized viewsComplex aggregations queried oftenPre-computed. Must refresh. Good middle ground.

🔥 Transactions & Isolation Levels

A transaction groups multiple operations into an atomic unit — all succeed or all fail. Isolation levels control what concurrent transactions can see.

LevelDirty ReadNon-repeatable ReadPhantom ReadUse case
Read Uncommitted✅ Possible✅ Possible✅ PossibleAlmost never used
Read Committed (PG default)❌ Prevented✅ Possible✅ PossibleMost OLTP workloads
Repeatable Read❌ Prevented✅ PossibleFinancial calculations
Serializable❌ PreventedCritical consistency (rare, slow)

Connection Pooling

Each Postgres connection uses ~10MB of memory. Opening a new connection per request is expensive (~50ms). A connection pool (PgBouncer, built-in pool in your ORM) maintains a set of reusable connections. Set pool size to ~(CPU cores × 2) + disk spindles. Too many connections = context switching overhead.

📝 Quick Revision

Quick Revision Cheat Sheet

Indexing: B-tree default. Index WHERE/JOIN/ORDER columns. EXPLAIN ANALYZE to verify.

Composite index: Leftmost prefix rule. (a, b) works for WHERE a, WHERE a AND b. Not WHERE b alone.

Isolation: Read Committed is PG default. Serializable for max consistency (slow).

Connection pool: Reuse connections. Pool size ≈ CPU cores × 2. Use PgBouncer in production.

EXPLAIN ANALYZE: Shows actual execution plan + timing. First tool for query optimization.

06

Caching Strategies

Caching is the single biggest performance lever in backend systems. The hard part isn't adding a cache — it's invalidating it correctly.

Redis Basics

Redis is an in-memory key-value store. Sub-millisecond reads. Supports strings, hashes, lists, sets, sorted sets, and streams. Common uses: caching, session storage, rate limiting, pub/sub, leaderboards. Data is in memory — use persistence (RDB/AOF) or accept data loss on restart.

🔥 Cache Invalidation — VERY IMPORTANT

The hardest problem in CS (after naming things)

Cache invalidation is where most caching bugs live. Stale data causes subtle, hard-to-reproduce issues. Know the strategies and their trade-offs — interviewers love this topic.

StrategyHow it worksConsistencyComplexity
TTL (Time-to-Live)Cache expires after N seconds. Simple but data can be stale for up to TTL.Eventual (within TTL)Low
Write-throughWrite to cache AND DB on every write. Cache is always fresh.StrongMedium — every write hits both
Write-back (write-behind)Write to cache first, async flush to DB. Fast writes.Eventual — risk of data lossHigh — need durability guarantees
Cache-aside (lazy loading)Read: check cache → miss → read DB → populate cache. Write: update DB → invalidate cache.Eventual (brief window)Medium — most common pattern
cache-aside.tstypescript
// Cache-aside pattern (most common)
async function getUser(id: string) {
  // 1. Check cache
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);

  // 2. Cache miss — read from DB
  const user = await db.query("SELECT * FROM users WHERE id = $1", [id]);

  // 3. Populate cache with TTL
  await redis.set(`user:${id}`, JSON.stringify(user), "EX", 300); // 5 min TTL

  return user;
}

// On update — invalidate (don't update) the cache
async function updateUser(id: string, data: Partial<User>) {
  await db.query("UPDATE users SET ... WHERE id = $1", [id]);
  await redis.del(`user:${id}`); // next read will repopulate
}

📝 Quick Revision

Quick Revision Cheat Sheet

Cache-aside: Read: cache → miss → DB → populate. Write: DB → invalidate cache. Most common.

Write-through: Write to both cache + DB. Always consistent. Slower writes.

Write-back: Write to cache, async to DB. Fast but risk of data loss.

TTL: Simplest invalidation. Set expiry. Accept staleness within window.

Cache stampede: Many requests hit DB simultaneously on cache miss. Fix: lock + single fetch.

07

System Design Basics

Backend system design tests whether you can think beyond a single server. These concepts come up in every senior-level interview.

Horizontal vs Vertical Scaling

AspectVertical (Scale Up)Horizontal (Scale Out)
WhatBigger machine (more CPU, RAM)More machines
LimitHardware ceilingVirtually unlimited
ComplexitySimple — same codeComplex — need load balancing, shared state
CostExpensive at high endCheaper commodity hardware
DowntimeRequires restartZero-downtime with rolling deploys

Load Balancing

A load balancer distributes incoming requests across multiple servers. Algorithms: Round Robin (simple rotation), Least Connections (send to least busy), IP Hash (sticky sessions), Weighted (more traffic to stronger servers). In production: Nginx, HAProxy, AWS ALB, or cloud load balancers.

Designing Scalable APIs

  • Stateless servers — no in-memory sessions. Store state in Redis/DB.
  • Database read replicas — write to primary, read from replicas.
  • Caching layer — Redis between app and DB for hot data.
  • Async processing — offload heavy work to queues (email, reports, image processing).
  • CDN for static assets — serve images, JS, CSS from edge servers.

📝 Quick Revision

Quick Revision Cheat Sheet

Scale up: Bigger machine. Simple but has a ceiling.

Scale out: More machines. Need load balancer + stateless servers.

Load balancer: Round robin, least connections, IP hash. Nginx/ALB in production.

Read replicas: Write to primary, read from replicas. Eventual consistency.

Stateless: No in-memory sessions. Store everything in Redis/DB.

08

Messaging & Async Processing

Not everything needs to happen in the request-response cycle. Async processing is how production systems handle email, notifications, reports, and any work that can be deferred.

When to Use Async Jobs

  • Sending emails/SMS/push notifications
  • Generating reports or PDFs
  • Image/video processing
  • Syncing data to third-party services
  • Any work that takes > 500ms and the user doesn't need the result immediately

Kafka vs RabbitMQ

AspectKafkaRabbitMQ
ModelDistributed log (append-only)Message broker (queue)
OrderingPer-partition ordering guaranteedPer-queue ordering
Replay✅ Consumers can re-read old messages❌ Messages deleted after ack
ThroughputVery high (millions/sec)High (tens of thousands/sec)
Best forEvent streaming, analytics, audit logsTask queues, RPC, simple pub/sub
ComplexityHigher (ZooKeeper/KRaft, partitions)Lower (simpler to operate)

Retry Mechanisms & Dead Letter Queues

Jobs fail. Network timeouts, service outages, bugs. A retry strategy handles transient failures: retry 3 times with exponential backoff (1s, 4s, 16s). After max retries, move the message to a Dead Letter Queue (DLQ) for manual inspection. Never retry indefinitely — you'll amplify the problem.

📝 Quick Revision

Quick Revision Cheat Sheet

Async when: Work > 500ms that user doesn't need immediately. Email, reports, processing.

Kafka: Distributed log. High throughput. Replay. For event streaming.

RabbitMQ: Message broker. Simpler. For task queues and RPC.

Retries: Exponential backoff. Max 3-5 retries. Then DLQ.

DLQ: Dead Letter Queue. Failed messages go here for manual review.

09

Spring Boot — Deep Dive

🔥 This section is the most important for Java backend roles

Spring Boot is the backbone of enterprise Java. Interviewers expect you to understand not just how to use it, but how it works internally — auto-configuration, bean lifecycle, request flow, and JPA pitfalls.

A. Core Architecture

How Spring Boot Works Internally

Spring Boot is an opinionated layer on top of Spring Framework. When you run @SpringBootApplication, three things happen: (1) @Configuration — marks the class as a bean definition source, (2) @EnableAutoConfiguration — triggers auto-config based on classpath, (3) @ComponentScan — scans the package for beans. Spring Boot reads META-INF/spring.factories (or spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports) to find auto-configuration classes.

🔥 Auto-Configuration — How It Decides

Auto-configuration uses @Conditional annotations to decide what to configure. For example, @ConditionalOnClass(DataSource.class) means "only configure this if DataSource is on the classpath." @ConditionalOnMissingBean means "only create this bean if the developer hasn't defined their own." This is why adding spring-boot-starter-data-jpa to your dependencies automatically configures a DataSource, EntityManager, and transaction manager — without any XML.

🔥 Dependency Injection — Deep Understanding

DI is not "Spring creates objects for you." It's inversion of control: instead of a class creating its dependencies, the container injects them. This enables testability (inject mocks), loose coupling (depend on interfaces, not implementations), and lifecycle management (singleton by default).

DependencyInjection.javatypescript
// Constructor injection (preferred — immutable, testable)
@Service
public class OrderService {
    private final OrderRepository repo;
    private final PaymentGateway gateway;

    // Spring auto-injects because there's only one constructor
    public OrderService(OrderRepository repo, PaymentGateway gateway) {
        this.repo = repo;
        this.gateway = gateway;
    }
}

// Why constructor injection > field injection (@Autowired on field):
// 1. Fields can be final (immutable)
// 2. Dependencies are explicit in the constructor signature
// 3. Easy to test — just pass mocks in the constructor
// 4. Fails fast if a dependency is missing (compile-time vs runtime)

Bean Lifecycle

Instantiate → Populate properties (DI) → @PostConstruct → Ready to use → @PreDestroy → Garbage collected. Scopes: singleton (default — one instance per container), prototype (new instance per injection), request/session (web scopes).

B. Application Design

Layered Architecture

Controller (HTTP layer — receives requests, returns responses) → Service (business logic — orchestrates operations) → Repository (data access — talks to DB). Each layer depends only on the layer below. Controllers never call repositories directly. Services never return HTTP responses.

DTO vs Entity Separation

Entities are JPA-managed objects mapped to DB tables. DTOs are plain objects for API input/output. Never expose entities directly in API responses — you'll leak internal fields, create tight coupling to the DB schema, and risk lazy-loading exceptions. Map between them in the service layer.

C. REST API Implementation

🔥 Request Handling Flow (DispatcherServlet)

Every request flows through: Filter chain DispatcherServletHandlerMapping (finds the right controller method) → HandlerAdapter (invokes it) → ControllerViewResolver (for MVC) or direct response (for REST). Interceptors run before/after the handler. Filters run before/after the entire servlet.

Interceptors vs Filters

AspectFilter (Servlet)Interceptor (Spring)
LevelServlet container levelSpring MVC level
Access toRaw request/responseHandler method, ModelAndView
Use caseLogging, CORS, security, compressionAuth checks, request timing, audit logging
OrderRuns before DispatcherServletRuns after DispatcherServlet, before handler

Exception Handling (@ControllerAdvice)

GlobalExceptionHandler.javatypescript
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
        return new ErrorResponse("NOT_FOUND", ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
        List<FieldError> errors = ex.getBindingResult().getFieldErrors()
            .stream()
            .map(f -> new FieldError(f.getField(), f.getDefaultMessage()))
            .toList();
        return new ErrorResponse("VALIDATION_ERROR", "Invalid input", errors);
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleGeneric(Exception ex) {
        log.error("Unhandled exception", ex);
        return new ErrorResponse("INTERNAL_ERROR", "Something went wrong");
    }
}

D. Data Layer (JPA / Hibernate)

🔥 Lazy vs Eager Loading — VERY IMPORTANT

AspectLazy (default for collections)Eager
When loadedOn first access (proxy)Immediately with parent query
Default for@OneToMany, @ManyToMany@ManyToOne, @OneToOne
RiskLazyInitializationException outside transactionLoading too much data (performance)
Best practiceUse lazy + fetch join when you need the dataAlmost never use eager on collections

🔥 The LazyInitializationException trap

This is the #1 JPA bug. You load an entity in a service method (inside a transaction), return it to the controller (outside the transaction), and try to access a lazy collection → exception. Fix: use DTOs (map in the service layer while the transaction is open), or use JOIN FETCH in your query.

🔥 N+1 Query Problem — MUST KNOW

You fetch 100 orders. Each order has a lazy-loaded user. When you access order.getUser() for each order, Hibernate fires 100 separate SELECT queries (1 for orders + 100 for users = N+1). This destroys performance.

N1Problem.javatypescript
// ❌ N+1 problem — 101 queries for 100 orders
List<Order> orders = orderRepo.findAll();
orders.forEach(o -> System.out.println(o.getUser().getName())); // 100 extra queries!

// ✅ Fix 1: JOIN FETCH in JPQL
@Query("SELECT o FROM Order o JOIN FETCH o.user")
List<Order> findAllWithUsers();

// ✅ Fix 2: @EntityGraph
@EntityGraph(attributePaths = {"user"})
List<Order> findAll();

// ✅ Fix 3: @BatchSize on the entity (Hibernate-specific)
@BatchSize(size = 50) // loads users in batches of 50 instead of 1-by-1
@OneToMany(mappedBy = "order")
private List<OrderItem> items;

🔥 @Transactional Deep Dive

@Transactional creates a proxy that wraps the method in a DB transaction. Key gotchas: (1) only works on public methods, (2) self-invocation bypasses the proxy (calling another @Transactional method from the same class doesn't create a new transaction), (3) checked exceptions don't trigger rollback by default — use @Transactional(rollbackFor = Exception.class).

PropagationBehaviorUse case
REQUIRED (default)Join existing tx or create newMost service methods
REQUIRES_NEWAlways create a new tx (suspend current)Audit logging that must persist even if parent fails
NESTEDSavepoint within current txPartial rollback within a larger operation
NOT_SUPPORTEDRun without tx (suspend current)Read-only operations that don't need tx overhead

E. Performance & Optimization

  • Connection pooling (HikariCP) — Spring Boot's default. Set maximum-pool-size to ~(CPU cores × 2). Monitor with /actuator/metrics.
  • Caching with Redis@Cacheable("users") on service methods. Use @CacheEvict on writes. Configure RedisCacheManager with TTL.
  • Reducing DB calls — batch inserts (saveAll), projection queries (select only needed columns), avoid N+1 with fetch joins.
  • Pagination — use Pageable parameter in repository methods. Return Page<DTO>, not List<Entity>.

F. Security

🔥 Spring Security — Filter Chain

Spring Security is a chain of servlet filters. Each filter handles one concern: CORS → CSRF → Authentication → Authorization → Exception handling. The SecurityFilterChain bean configures which endpoints require auth, which are public, and how authentication works (JWT, session, OAuth).

SecurityConfig.javatypescript
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable()) // disable for stateless APIs
            .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }
}

// JWT Filter — runs before Spring's auth filter
// 1. Extract token from Authorization header
// 2. Validate token (signature, expiry)
// 3. Set SecurityContext with user details
// 4. Chain continues — Spring Security checks roles/permissions

G. Advanced Topics

  • AOP (Aspect-Oriented Programming) — cross-cutting concerns (logging, timing, security) without modifying business code. @Aspect + @Around/@Before/@After. Spring Security and @Transactional are built on AOP.
  • Custom annotations — combine @Target, @Retention with an AOP aspect to create reusable behaviors like @RateLimit or @Audit.
  • Profiles@Profile("dev") / @Profile("prod") to swap beans per environment. Use application-dev.yml for env-specific config.
  • Microservices role — Spring Boot is the standard for building individual microservices. Each service is a standalone Boot app with its own DB, deployed independently. Spring Cloud adds service discovery, config server, circuit breakers.

📝 Spring Boot Quick Revision

Quick Revision Cheat Sheet

Auto-config: @Conditional annotations. Classpath detection. Override with your own beans.

DI: Constructor injection preferred. Immutable, testable, fails fast.

Request flow: Filter → DispatcherServlet → HandlerMapping → Controller → Response.

N+1: Lazy collections cause N extra queries. Fix: JOIN FETCH or @EntityGraph.

@Transactional: Proxy-based. Public methods only. Self-invocation bypasses proxy.

Lazy loading: Default for collections. Access outside tx = LazyInitException. Use DTOs.

Security: Filter chain. JWT filter before auth filter. SecurityFilterChain bean.

10

Top 30 Interview Questions

These are the questions that actually get asked

Compiled from real interview experiences at product companies. Grouped by topic. Practice answering each in 2-3 minutes.

Node.js (1-5)

Q:1. How does the Node.js event loop work? How is it different from the browser?

A: Node's event loop has 6 phases (timers, pending, idle, poll, check, close). Browser's is simpler (task → microtasks → render). Node has process.nextTick (highest priority) and setImmediate (check phase). Both drain microtasks between phases.

Q:2. What happens when you do CPU-intensive work in Node?

A: It blocks the event loop — no other requests can be processed. Solutions: Worker Threads for parallel computation, child_process.fork for separate processes, or offload to a dedicated service. Never do heavy computation on the main thread.

Q:3. Explain streams in Node.js. When would you use them?

A: Streams process data in chunks without loading everything into memory. Use for large files, HTTP responses, real-time data. Four types: Readable, Writable, Duplex, Transform. pipeline() handles backpressure and error propagation.

Q:4. How do you scale a Node.js application?

A: Cluster module (fork per CPU core), PM2 for process management, horizontal scaling with load balancer (Nginx/ALB), containerization (Docker + K8s), and stateless design (sessions in Redis, not memory).

Q:5. What is the difference between process.nextTick and Promise.resolve().then()?

A: Both are microtasks, but nextTick has higher priority — it runs before Promise microtasks. nextTick queue is drained completely before moving to the Promise microtask queue. Overusing nextTick can starve I/O.

API & Auth (6-10)

Q:6. How do you design a RESTful API for a resource with nested relationships?

A: Use nested routes for strong ownership (/users/123/orders), flat routes for independent resources (/orders?user_id=123). Keep nesting to max 2 levels. Use HATEOAS links for discoverability. Always version your API (/v1/).

Q:7. JWT vs session-based auth — when would you pick each?

A: JWT for stateless APIs, microservices, mobile apps (no server-side session store needed). Sessions for traditional web apps where you need easy revocation. JWT trade-off: can't revoke until expiry without a blocklist.

Q:8. How do you handle API rate limiting in a distributed system?

A: Use Redis with sliding window counter (sorted sets). Key by user ID or API key. Implement at API gateway level for global limits, application middleware for per-endpoint limits. Return 429 with Retry-After header.

Q:9. What is idempotency and why does it matter?

A: An idempotent operation produces the same result regardless of how many times it's called. GET, PUT, DELETE are idempotent. POST is not. Matters for retries — network failures happen. Make POST idempotent with idempotency keys (UUID in header).

Q:10. How do you handle pagination for a large dataset?

A: Offset-based (?page=3&limit=20) for simple UIs with page numbers. Cursor-based (?cursor=abc&limit=20) for infinite scroll — faster at depth, consistent with concurrent inserts. Always return total count and next cursor in response meta.

Database (11-16)

Q:11. When should you add an index? When should you NOT?

A: Add: columns in WHERE, JOIN, ORDER BY with high cardinality and read-heavy workload. Don't: small tables, low-cardinality columns (boolean), write-heavy tables (indexes slow writes). Always EXPLAIN ANALYZE to verify.

Q:12. Explain database transaction isolation levels.

A: Read Committed (PG default): no dirty reads. Repeatable Read: same query returns same results within a tx. Serializable: full isolation, slowest. Trade-off: higher isolation = more locking = lower throughput.

Q:13. What is connection pooling and why is it important?

A: Each DB connection uses ~10MB RAM and ~50ms to establish. A pool maintains reusable connections. Set size to ~(CPU cores × 2). Too many = context switching. Too few = request queuing. HikariCP is the standard for Java.

Q:14. How do you optimize a slow SQL query?

A: 1) EXPLAIN ANALYZE to see the plan. 2) Add missing indexes. 3) Avoid SELECT * — select only needed columns. 4) Rewrite subqueries as JOINs. 5) Consider denormalization or materialized views for complex aggregations.

Q:15. Joins vs denormalization — how do you decide?

A: Normalize when data integrity matters and reads are moderate. Denormalize when reads vastly outnumber writes and you need sub-ms response. Materialized views are a middle ground — pre-computed but refreshable.

Q:16. What is the N+1 query problem and how do you fix it?

A: Fetching N parent entities, then lazily loading a child for each = N+1 queries. Fix: JOIN FETCH in JPQL, @EntityGraph in Spring Data, @BatchSize for Hibernate batching, or use DTOs with explicit joins.

Caching & System Design (17-22)

Q:17. Explain cache-aside pattern.

A: Read: check cache → miss → read DB → populate cache. Write: update DB → invalidate cache (delete, don't update). Most common pattern. Risk: brief inconsistency window between DB write and cache invalidation.

Q:18. How do you handle cache invalidation?

A: TTL for simplicity (accept staleness). Event-driven invalidation for consistency (publish event on write, subscriber invalidates cache). Write-through for strong consistency (write to both). Cache stampede prevention with locks.

Q:19. How would you design a system to handle 10K requests/second?

A: Stateless app servers behind a load balancer. Redis cache for hot data. Read replicas for DB. Async processing for heavy work (queues). CDN for static assets. Connection pooling. Horizontal scaling.

Q:20. Horizontal vs vertical scaling — trade-offs?

A: Vertical: simpler (same code), has a ceiling, requires downtime. Horizontal: virtually unlimited, needs stateless design + load balancer + shared state (Redis), zero-downtime deploys. Most production systems use horizontal.

Q:21. When would you use a message queue?

A: When work can be deferred (email, notifications, reports), when you need to decouple services, when you need retry/DLQ for reliability, when you need to smooth traffic spikes (queue absorbs bursts).

Q:22. Kafka vs RabbitMQ — when to use which?

A: Kafka: event streaming, audit logs, high throughput, replay capability. RabbitMQ: task queues, RPC, simpler operations, when you don't need replay. Kafka for 'what happened', RabbitMQ for 'do this task'.

Spring Boot (23-30)

Q:23. How does Spring Boot auto-configuration work?

A: Spring Boot scans META-INF/spring.factories for auto-config classes. Each uses @Conditional annotations (@ConditionalOnClass, @ConditionalOnMissingBean) to decide whether to activate. Your explicit beans always take priority over auto-configured ones.

Q:24. Why is constructor injection preferred over field injection?

A: Constructor injection: fields can be final (immutable), dependencies are explicit, easy to test (pass mocks), fails fast if missing. Field injection: requires reflection, can't be final, hides dependencies, harder to test.

Q:25. Explain the N+1 problem in JPA and how to fix it.

A: Lazy-loaded collections cause N extra queries when accessed in a loop. Fix: @Query with JOIN FETCH, @EntityGraph, @BatchSize, or map to DTOs with explicit joins in the service layer.

Q:26. What are the gotchas with @Transactional?

A: 1) Only works on public methods (proxy limitation). 2) Self-invocation bypasses the proxy. 3) Checked exceptions don't rollback by default. 4) Propagation matters — REQUIRED joins existing tx, REQUIRES_NEW creates a new one.

Q:27. How does Spring Security's filter chain work?

A: A chain of servlet filters: CORS → CSRF → Authentication (extract credentials) → Authorization (check permissions) → Exception handling. JWT auth adds a custom filter before the default auth filter to validate tokens and set SecurityContext.

Q:28. Explain lazy vs eager loading in JPA.

A: Lazy: loads on first access (proxy). Default for @OneToMany/@ManyToMany. Risk: LazyInitializationException outside transaction. Eager: loads immediately. Default for @ManyToOne/@OneToOne. Risk: loading too much data. Best practice: lazy + fetch join when needed.

Q:29. How do you handle exceptions globally in Spring Boot?

A: @RestControllerAdvice with @ExceptionHandler methods. Map custom exceptions to HTTP status codes. Return consistent error response shape. Catch generic Exception as a fallback. Log unhandled exceptions with request context.

Q:30. What is AOP and where is it used in Spring?

A: Aspect-Oriented Programming — add cross-cutting behavior (logging, security, transactions) without modifying business code. @Transactional is AOP (proxy wraps method in tx). @Cacheable is AOP. Spring Security filters use AOP concepts.

11

Common Mistakes Full Stack Devs Make in Backend

🔴 Treating the database like a dumb store

Writing all logic in the application layer and using the DB only for CRUD. Missing indexes, ignoring query plans, not using transactions properly. The DB is a powerful engine — use its features (indexes, constraints, triggers, materialized views).

🔴 Not thinking about failure cases

Happy path works, but what happens when the DB is down? When a third-party API times out? When Redis is full? Production systems need retries, circuit breakers, fallbacks, and graceful degradation. Always ask: "what if this fails?"

🔴 Exposing JPA entities in API responses

Returning entities directly leaks internal fields, creates tight coupling to the DB schema, and causes LazyInitializationException. Always map to DTOs in the service layer.

🔴 Ignoring the N+1 problem

Works fine in dev with 10 rows. Destroys performance in production with 10K rows. Always check query count with Hibernate logging or p6spy. Use JOIN FETCH or @EntityGraph.

🔴 Over-engineering with microservices

Starting with microservices for a new project with 2 developers. A well-structured monolith is faster to develop, easier to debug, and simpler to deploy. Extract services only when you have a clear scaling or team boundary reason.

🔴 Not understanding @Transactional

Putting @Transactional on private methods (doesn't work), calling @Transactional methods from the same class (self-invocation bypasses proxy), not handling rollback for checked exceptions. These are the top 3 Spring transaction bugs.

12

How to Answer Backend Questions

Backend interviews reward structured thinking. Here's the framework that works for both coding and system design questions.

The STAR-T Framework for Backend

1. Scope — Clarify requirements

What's the scale? How many users/requests? What's the consistency requirement? Is it read-heavy or write-heavy? What are the SLAs? Asking these questions shows you think like a senior engineer.

2. Trade-offs — State alternatives

Never present one solution. Say: "We could use approach A (pros/cons) or approach B (pros/cons). Given our requirements, I'd go with A because..." This is the #1 signal interviewers look for.

3. Architecture — Draw the system

Start with the high-level components: client → load balancer → app servers → cache → database. Then zoom into the specific area the question focuses on. Top-down, not bottom-up.

4. Risks — Address failure modes

What happens if the cache goes down? If the DB is slow? If a service is unavailable? Mention retries, circuit breakers, fallbacks, monitoring. This separates mid from senior.

5. Testing — How would you verify?

Mention load testing, integration tests, monitoring (metrics, alerts). "I'd set up alerts on p99 latency and error rate, and load test to 2x expected traffic before launch."

💡 The golden rule

When you don't know something, say: "I haven't worked with that directly, but based on my understanding of [related concept], I'd approach it like..." Interviewers respect honesty + reasoning over bluffing.

13

Last Day Revision Sheet

Skim this 30 minutes before your interview

This won't replace practice, but it'll prime your brain so the right concepts surface faster under pressure.

Node.js

Quick Revision Cheat Sheet

Event loop: 6 phases. nextTick > microtask > timer > immediate.

Non-blocking: libuv handles I/O. Main thread never blocks (unless CPU work).

Streams: Process chunks. Readable, Writable, Duplex, Transform.

Scaling: Cluster per core. PM2 or K8s. Stateless design.

API & Auth

Quick Revision Cheat Sheet

REST: Nouns for resources. Verbs for actions. Stateless. Consistent errors.

Idempotency: GET/PUT/DELETE safe to retry. POST needs idempotency key.

JWT: Stateless. Hard to revoke. Short expiry + refresh tokens.

Rate limiting: Token bucket or sliding window. Redis for distributed. 429 + Retry-After.

Database

Quick Revision Cheat Sheet

Indexing: B-tree. WHERE/JOIN/ORDER columns. EXPLAIN ANALYZE. Leftmost prefix.

Isolation: Read Committed default. Serializable for max consistency.

N+1: Lazy collections + loop = N extra queries. JOIN FETCH to fix.

Pool: HikariCP. Size ≈ cores × 2. Reuse connections.

Caching

Quick Revision Cheat Sheet

Cache-aside: Read: cache → miss → DB → populate. Write: DB → invalidate.

Invalidation: TTL (simple), event-driven (consistent), write-through (strong).

Stampede: Many requests hit DB on miss. Fix: lock + single fetch.

Spring Boot

Quick Revision Cheat Sheet

Auto-config: @Conditional. Classpath detection. Your beans override.

DI: Constructor injection. Immutable. Testable. Fails fast.

Request flow: Filter → DispatcherServlet → Handler → Controller → Response.

N+1 fix: JOIN FETCH, @EntityGraph, @BatchSize, or DTO projection.

@Transactional: Public only. No self-invocation. rollbackFor for checked exceptions.

Lazy loading: Default for collections. Access outside tx = exception. Use DTOs.

Security: Filter chain. JWT filter before auth. SecurityFilterChain bean.

System Design

Quick Revision Cheat Sheet

Scale out: Stateless servers + load balancer + Redis + read replicas.

Async: Queue heavy work. Retry with backoff. DLQ for failures.

Answer framework: Scope → Trade-offs → Architecture → Risks → Testing.