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.
Table of Contents
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.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
| Feature | Node.js | Browser |
|---|---|---|
| Event loop | 6 phases (libuv) | Task → microtasks → render |
| nextTick | ✅ process.nextTick (highest priority) | ❌ Not available |
| setImmediate | ✅ Check phase | ❌ Not standard |
| I/O | libuv thread pool (fs, dns, crypto) | Web APIs (fetch, setTimeout) |
| Threads | Worker 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.
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'.
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
| Strategy | How it works | Pros | Cons |
|---|---|---|---|
| Offset-based | ?page=3&limit=20 → OFFSET 40 LIMIT 20 | Simple, supports jump-to-page | Slow on large offsets, inconsistent with inserts |
| Cursor-based | ?cursor=abc123&limit=20 → WHERE id > cursor | Fast at any depth, consistent | No jump-to-page, cursor must be opaque |
| Keyset | ?after_id=500&limit=20 | Like cursor but with a real column | Requires 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
// 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.
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
| Aspect | JWT (Token-based) | Session (Server-side) |
|---|---|---|
| State | Stateless — token contains all claims | Stateful — session stored on server (Redis/DB) |
| Storage | Client (localStorage, cookie, memory) | Server (session store) + cookie (session ID) |
| Scalability | Easy — no shared state between servers | Harder — need shared session store |
| Revocation | Hard — can't invalidate until expiry | Easy — delete from session store |
| Size | Larger (payload + signature) | Small cookie (just session ID) |
| Best for | APIs, microservices, mobile apps | Traditional 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.
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
| Algorithm | How it works | Pros | Cons |
|---|---|---|---|
| Token Bucket | Bucket 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 Log | Store timestamp of each request. Count requests in the last N seconds. | Precise. No boundary issues. | Memory-heavy (stores every timestamp). |
| Sliding Window Counter | Combine current + previous window counts weighted by overlap. | Memory-efficient. Smooth. | Approximate — not perfectly precise. |
| Fixed Window | Count requests per fixed time window (e.g., per minute). | Simplest to implement. | Boundary spike: 2x burst at window edges. |
| Leaky Bucket | Requests enter a queue. Processed at a fixed rate. Queue overflow = reject. | Smooth output rate. | Doesn't allow any bursts. |
Where to Implement
| Layer | When to use | Trade-off |
|---|---|---|
| API Gateway (Nginx, Kong, AWS API GW) | Global rate limiting across all services | Coarse-grained. Can't do per-user logic easily. |
| Application middleware | Per-user, per-endpoint, custom logic | More flexible. Adds latency. Need shared store (Redis). |
| Load balancer | Connection-level limiting | Very fast. Limited to IP-based rules. |
// 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.
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 frequently | Table 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 workload | Write-heavy workload (indexes slow down INSERT/UPDATE) |
| You need to enforce uniqueness | You're indexing every column 'just in case' |
-- 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
| Approach | When to use | Trade-off |
|---|---|---|
| Normalized + JOINs | Data integrity matters, moderate read load | Slower reads, faster writes, no data duplication |
| Denormalized | Read-heavy, need sub-ms response, analytics | Faster reads, slower writes, data can go stale |
| Materialized views | Complex aggregations queried often | Pre-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.
| Level | Dirty Read | Non-repeatable Read | Phantom Read | Use case |
|---|---|---|---|---|
| Read Uncommitted | ✅ Possible | ✅ Possible | ✅ Possible | Almost never used |
| Read Committed (PG default) | ❌ Prevented | ✅ Possible | ✅ Possible | Most OLTP workloads |
| Repeatable Read | ❌ | ❌ Prevented | ✅ Possible | Financial calculations |
| Serializable | ❌ | ❌ | ❌ Prevented | Critical 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.
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.
| Strategy | How it works | Consistency | Complexity |
|---|---|---|---|
| TTL (Time-to-Live) | Cache expires after N seconds. Simple but data can be stale for up to TTL. | Eventual (within TTL) | Low |
| Write-through | Write to cache AND DB on every write. Cache is always fresh. | Strong | Medium — every write hits both |
| Write-back (write-behind) | Write to cache first, async flush to DB. Fast writes. | Eventual — risk of data loss | High — 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 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.
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
| Aspect | Vertical (Scale Up) | Horizontal (Scale Out) |
|---|---|---|
| What | Bigger machine (more CPU, RAM) | More machines |
| Limit | Hardware ceiling | Virtually unlimited |
| Complexity | Simple — same code | Complex — need load balancing, shared state |
| Cost | Expensive at high end | Cheaper commodity hardware |
| Downtime | Requires restart | Zero-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.
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
| Aspect | Kafka | RabbitMQ |
|---|---|---|
| Model | Distributed log (append-only) | Message broker (queue) |
| Ordering | Per-partition ordering guaranteed | Per-queue ordering |
| Replay | ✅ Consumers can re-read old messages | ❌ Messages deleted after ack |
| Throughput | Very high (millions/sec) | High (tens of thousands/sec) |
| Best for | Event streaming, analytics, audit logs | Task queues, RPC, simple pub/sub |
| Complexity | Higher (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.
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).
// 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 → DispatcherServlet → HandlerMapping (finds the right controller method) → HandlerAdapter (invokes it) → Controller → ViewResolver (for MVC) or direct response (for REST). Interceptors run before/after the handler. Filters run before/after the entire servlet.
Interceptors vs Filters
| Aspect | Filter (Servlet) | Interceptor (Spring) |
|---|---|---|
| Level | Servlet container level | Spring MVC level |
| Access to | Raw request/response | Handler method, ModelAndView |
| Use case | Logging, CORS, security, compression | Auth checks, request timing, audit logging |
| Order | Runs before DispatcherServlet | Runs after DispatcherServlet, before handler |
Exception Handling (@ControllerAdvice)
@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
| Aspect | Lazy (default for collections) | Eager |
|---|---|---|
| When loaded | On first access (proxy) | Immediately with parent query |
| Default for | @OneToMany, @ManyToMany | @ManyToOne, @OneToOne |
| Risk | LazyInitializationException outside transaction | Loading too much data (performance) |
| Best practice | Use lazy + fetch join when you need the data | Almost 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.
// ❌ 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).
| Propagation | Behavior | Use case |
|---|---|---|
| REQUIRED (default) | Join existing tx or create new | Most service methods |
| REQUIRES_NEW | Always create a new tx (suspend current) | Audit logging that must persist even if parent fails |
| NESTED | Savepoint within current tx | Partial rollback within a larger operation |
| NOT_SUPPORTED | Run 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-sizeto ~(CPU cores × 2). Monitor with/actuator/metrics. - Caching with Redis —
@Cacheable("users")on service methods. Use@CacheEvicton writes. ConfigureRedisCacheManagerwith TTL. - Reducing DB calls — batch inserts (
saveAll), projection queries (select only needed columns), avoid N+1 with fetch joins. - Pagination — use
Pageableparameter in repository methods. ReturnPage<DTO>, notList<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).
@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@Transactionalare built on AOP. - Custom annotations — combine
@Target,@Retentionwith an AOP aspect to create reusable behaviors like@RateLimitor@Audit. - Profiles —
@Profile("dev")/@Profile("prod")to swap beans per environment. Useapplication-dev.ymlfor 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.
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.
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.
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.
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.