Hi, I'm Suraj
>Home>Projects>Workbench>Interview Prep>Blog>Resume
GitHubXLinkedIn
status: building
>Home>Projects>Workbench>Interview Prep>Blog
status: building

Connect

Let's build something together

Always interested in collaborations, interesting problems, and conversations about code, design, and everything in between.

send a signal→

Find me elsewhere

GitHub
@SurajGavali
X
@surajgavali1111
LinkedIn
/in/SurajGavali
Email
surajgavali1601+hello@gmail.com
Forged with& code

© 2026 SurajGavali — All experiments reserved

Knowledge Base

Interview Prep

A curated collection of interview topics and deep-dives from a comprehensive Gemini-guided preparation session. Covers Spring Boot, Java internals, System Design, and competitive coding patterns.

View original Gemini thread

Spring Bean Lifecycle

Think of ordering a burger in a high-end restaurant — the container (Spring IoC) handles everything from instantiation to cleanup, just like the kitchen manages your order from raw ingredients to serving and clearing.

  • 7 critical phases: Instantiation → Property Population → Awareness → Pre-Init (BeanPostProcessor) → Init (@PostConstruct) → Post-Init (Proxy creation) → Destruction (@PreDestroy)
  • Constructor injection vs Field injection: constructor injection makes dependencies explicit and supports immutability
  • @PostConstruct runs after all dependencies are injected — use it for initialization logic that needs fully wired beans
  • Post-initialization is when Spring creates proxies for @Transactional, @Cacheable, etc.
java
@Component
public class OrderService {

    private final PaymentGateway paymentGateway;

    // Constructor Injection (recommended)
    public OrderService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }

    @PostConstruct
    public void init() {
        // Dependencies are fully injected here
        log.info("OrderService initialized with {}", paymentGateway);
    }

    @PreDestroy
    public void cleanup() {
        log.info("OrderService shutting down...");
    }
}

Interview Tip

Interviewers love asking 'Why not just put init logic in the constructor?' — because with field injection, dependencies aren't available yet in the constructor. @PostConstruct guarantees everything is wired.

Circular Dependency

The "Locked Box" problem: Bean A needs Bean B to be created, and Bean B needs Bean A — a classic deadlock.

  • @Lazy annotation: injects a proxy placeholder that resolves the real bean on first use
  • Setter/Field injection: allows Spring to instantiate both beans first, then inject
  • Best solution: refactor — extract shared logic into a third service to break the cycle
  • Spring Boot 2.6+ throws error by default on circular deps (spring.main.allow-circular-references=false)

Interview Tip

The best answer is always 'refactor the architecture.' Mention @Lazy as a quick fix but emphasize it's a code smell.

@Transactional Deep Dive

The "ATM Withdrawal" — either the money comes out AND your balance updates, or neither happens. All or nothing.

  • Spring wraps your class in a CGLIB proxy — the proxy manages begin/commit/rollback
  • Default rollback: only on RuntimeException (unchecked). Use rollbackFor = Exception.class for checked exceptions
  • Propagation: REQUIRED (join existing TX) vs REQUIRES_NEW (suspend current, start fresh)
  • Self-invocation trap: calling a @Transactional method from within the same class bypasses the proxy entirely
java
@Service
public class BankService {

    @Transactional(rollbackFor = Exception.class)
    public void transfer(Account from, Account to, BigDecimal amount) {
        from.debit(amount);   // Step 1
        to.credit(amount);    // Step 2
        // If Step 2 fails → Step 1 is rolled back automatically
    }

    // ⚠️ SELF-INVOCATION TRAP
    public void processPayroll(List<Employee> employees) {
        for (Employee emp : employees) {
            this.transfer(company, emp, emp.getSalary());
            // ❌ 'this' bypasses the proxy → NO transaction!
        }
    }
}

Interview Tip

Self-invocation is the #1 @Transactional interview trap. The fix? Inject the service into itself, or move the method to another service.

Global Exception Handling

  • @RestControllerAdvice provides centralized exception handling across all controllers
  • @ExceptionHandler methods catch specific exceptions and return consistent error responses
  • MethodArgumentNotValidException handles @Valid annotation failures with field-level errors
  • Always return structured error DTOs with timestamp, status, message, and path
java
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(
            ResourceNotFoundException ex, HttpServletRequest request) {
        ErrorResponse error = new ErrorResponse(
            LocalDateTime.now(),
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            request.getRequestURI()
        );
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors()
          .forEach(e -> errors.put(e.getField(), e.getDefaultMessage()));
        // Return 400 with field-level error details
        return ResponseEntity.badRequest().body(
            new ErrorResponse(errors)
        );
    }
}

Bean Scopes

  • Singleton (default): one instance per Spring container — shared across all injections
  • Prototype: new instance every time it's requested from the container
  • Trap: injecting a Prototype bean into a Singleton gives you a single instance forever
  • Fix: use ObjectProvider<T> or @Lookup to get fresh Prototype instances

Interview Tip

The Singleton-in-Prototype trap is a common gotcha. If asked 'What happens when you inject Prototype into Singleton?', the answer is: you get one fixed instance, not a new one each time.

ExecutorService — The Thread Pool

A "Taxi Fleet" — instead of hiring a new driver for every passenger (creating a thread per task), you maintain a fleet and assign available taxis.

  • Prevents OOM from unbounded thread creation
  • FixedThreadPool: exact number of threads, tasks queue when all busy
  • CachedThreadPool: creates threads on demand, reuses idle ones (risky for unbounded workloads)
  • Always shutdown() the pool — or use try-with-resources in Java 21
java
ExecutorService pool = Executors.newFixedThreadPool(10);

// Submit tasks
Future<String> future = pool.submit(() -> {
    Thread.sleep(1000);
    return "Task completed!";
});

// Get result (blocks until ready)
String result = future.get();

pool.shutdown();

CompletableFuture — Non-Blocking Chains

A "Restaurant Buzzer" — place your order (submit task), get a buzzer, go sit down. The buzzer rings when food is ready.

  • thenApply(): transform the result (map)
  • thenCompose(): chain dependent async operations (flatMap)
  • thenCombine(): merge results from two independent futures
  • exceptionally(): handle errors in the chain without try-catch
  • allOf(): wait for multiple futures to complete in parallel
java
CompletableFuture<Double> priceFuture =
    CompletableFuture.supplyAsync(() -> fetchPrice("AAPL"))
        .thenApply(price -> price * 1.1)          // Add 10% markup
        .thenCombine(
            CompletableFuture.supplyAsync(() -> fetchExchangeRate("USD")),
            (price, rate) -> price * rate           // Convert currency
        )
        .exceptionally(ex -> {
            log.error("Failed", ex);
            return 0.0;
        });

Interview Tip

Explain thenApply vs thenCompose as map vs flatMap — interviewers love the functional programming parallel.

@Async in Spring

  • Annotate a method with @Async to run it in a separate thread
  • Enable with @EnableAsync on a configuration class
  • Returns void or CompletableFuture<T> — never call .get() on @Async methods in the same class
  • Same self-invocation trap as @Transactional — proxy-based, so internal calls won't be async
  • Configure a custom thread pool with @Bean TaskExecutor to control pool size

Virtual Threads (Java 21)

Moving from a fleet of expensive taxis (platform threads) to millions of lightweight drones — each "drone" handles one request but costs almost nothing.

  • Project Loom: virtual threads are managed by the JVM, not the OS
  • 1 million virtual threads ≈ a few MB of memory (vs GBs for platform threads)
  • Perfect for I/O-bound workloads (HTTP calls, DB queries, file operations)
  • Spring Boot 3.2+: set spring.threads.virtual.enabled=true
  • Don't use for CPU-bound tasks — they still share the same carrier threads
java
// Java 21 — create virtual threads directly
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 100_000).forEach(i ->
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        })
    );
} // Automatically shuts down

Interview Tip

Virtual Threads are the hottest Java 21 topic. Key point: they're NOT faster for CPU work — they shine for high-concurrency I/O workloads where threads spend most time waiting.

Java 8 — The Foundation

  • Lambda expressions: (a, b) -> a + b
  • Stream API: filter, map, reduce, collect — declarative data processing
  • Optional<T>: explicit null-safety without NPE chains
  • Default methods in interfaces: add behavior without breaking implementations
  • java.time API: LocalDate, Instant, Duration replacing legacy Date/Calendar

Java 11 — Modernization

  • var keyword (local variables): var list = new ArrayList<String>()
  • Standard HTTP Client: HttpClient replacing Apache HttpClient for many use cases
  • String enhancements: isBlank(), strip(), lines(), repeat()
  • Files.readString() / writeString(): one-line file I/O
  • Running single-file programs: java HelloWorld.java (no javac step)

Java 17 — The LTS Leap

  • Records: public record User(String name, int age) {} — immutable data carriers, auto-generates equals/hashCode/toString
  • Sealed classes: restrict which classes can extend/implement (sealed interface Shape permits Circle, Square)
  • Pattern matching for instanceof: if (obj instanceof String s) — no cast needed
  • Text blocks: triple-quote strings with proper indentation handling
  • Switch expressions: arrow syntax with yield for complex cases
java
// Record — replaces entire POJO with getters, equals, hashCode, toString
public record UserDTO(String name, String email, int age) {}

// Sealed interface
public sealed interface Shape permits Circle, Rectangle {
    double area();
}

// Pattern matching
if (shape instanceof Circle c) {
    System.out.println("Radius: " + c.radius());
}

// Text block
String json = """
    {
        "name": "Suraj",
        "role": "SDE-2"
    }
    """;

Java 21 — The Game Changer

  • Virtual Threads (Project Loom): lightweight threads for massive concurrency
  • Sequenced Collections: SequencedCollection, SequencedMap with getFirst()/getLast()/reversed()
  • Record Patterns: deconstruct records in switch/if statements
  • String Templates (preview): STR."Hello \{name}" — type-safe string interpolation
  • Scoped Values: thread-safe alternative to ThreadLocal for virtual threads

Interview Tip

When asked 'What Java version are you on?', always mention the migration path. E.g., 'We migrated from 11 to 17 for Records and sealed classes, and are evaluating 21 for virtual threads.'

Horizontal vs Vertical Scaling

  • Vertical scaling (scale up): bigger machine — more CPU/RAM. Simple but has a ceiling.
  • Horizontal scaling (scale out): more machines behind a load balancer. Unlimited ceiling but adds complexity.
  • Stateless APIs → horizontal scaling is easy. Stateful services (sessions, caches) → need sticky sessions or external state stores.
  • Database: start vertical, then shard horizontally when vertical ceiling is hit.
  • Decision framework: 'Can I just add more pods?' → horizontal. 'Do I need more RAM per instance?' → vertical.

Kafka Architecture Deep Dive

  • Topics: logical channels for messages (e.g., order-events, payment-events)
  • Partitions: physical splits within a topic — enable parallelism and ordering guarantees per partition
  • Consumer Groups: a set of consumers sharing the workload of a topic. Each partition → exactly one consumer in the group.
  • Golden ratio: 1 partition = 1 consumer pod = maximum parallelism. 50 partitions → scale up to 50 pods.
  • If consumers > partitions: extra consumers sit idle. If partitions > consumers: some consumers handle multiple partitions.

Interview Tip

Key insight: Kafka guarantees ordering WITHIN a partition, not across partitions. Use a consistent partition key (e.g., user_id) to ensure all events for one user go to the same partition.

Kafka Rebalancing

  • Triggered when consumers join/leave a group (deployment, scaling, crash)
  • Eager Rebalancing (default): 'Stop the World' — ALL consumers pause, ALL partitions are revoked and reassigned
  • Cooperative/Incremental Rebalancing: only affected partitions are revoked — others keep processing
  • During rebalancing: no messages are consumed → latency spike
  • Best practice: use Cooperative rebalancing + configure session.timeout.ms and heartbeat.interval.ms carefully

CosmosDB Performance Tuning

  • Request Units (RUs): CosmosDB's currency — every read/write costs RUs based on document size and query complexity
  • Pagination: increasing MaxItemCount reduces round-trips but risks hitting the 4MB response limit
  • Cross-partition queries are expensive — always include the partition key in WHERE clauses
  • Use point-reads (by id + partition key) instead of queries when possible — 1 RU vs 5+ RUs
  • Monitor with Azure Diagnostics: track RU consumption, latency percentiles, and throttling (429 responses)

Circuit Breaker Pattern (Resilience4j)

  • Three states: CLOSED (normal flow) → OPEN (all calls fail-fast) → HALF_OPEN (test a few calls)
  • Prevents cascading failures when a downstream service is down
  • Configure thresholds: failureRateThreshold (e.g., 50%), waitDurationInOpenState (e.g., 30s), permittedNumberOfCallsInHalfOpenState (e.g., 3)
  • Combine with fallback methods to return cached/default responses during outages
  • Use with @Retry for transient failures and @Bulkhead for thread isolation
java
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
@Retry(name = "paymentService", fallbackMethod = "paymentFallback")
public PaymentResponse processPayment(PaymentRequest request) {
    return paymentClient.charge(request);
}

public PaymentResponse paymentFallback(PaymentRequest request, Throwable t) {
    log.warn("Payment service unavailable, returning cached response", t);
    return PaymentResponse.pending(request.getOrderId());
}

Transaction Isolation Levels

  • READ_UNCOMMITTED: can read uncommitted changes (dirty reads). Fastest, least safe.
  • READ_COMMITTED (default for PostgreSQL): only reads committed data. Prevents dirty reads.
  • REPEATABLE_READ: locks rows for the duration of the transaction. Prevents non-repeatable reads.
  • SERIALIZABLE: full isolation — transactions execute as if sequential. Prevents phantom reads but lowest throughput.
  • Trade-off: higher isolation = more locking = lower throughput. Most apps use READ_COMMITTED.

Interview Tip

Know the three anomalies: Dirty Read (reading uncommitted data), Non-Repeatable Read (row changes between two reads), Phantom Read (new rows appear between two identical queries).

SQL vs NoSQL Decision Framework

  • SQL (relational): structured data, complex JOINs, ACID guarantees, strong consistency. Best for: financial systems, ERP, anything with complex relationships.
  • NoSQL (document/key-value/graph): flexible schema, horizontal scaling, eventual consistency. Best for: real-time analytics, content management, IoT telemetry.
  • CAP theorem: you can only guarantee 2 of 3 — Consistency, Availability, Partition tolerance. SQL favors CP, most NoSQL favors AP.
  • Hybrid: use SQL for transactional data + NoSQL for read-heavy analytics (CQRS pattern).

SQL Injection Prevention

  • NEVER concatenate user input into SQL strings
  • Use PreparedStatement: parameterized queries that separate code from data
  • Allow-listing: validate/whitelist column names for dynamic ORDER BY
  • ORM frameworks (Hibernate/JPA) use parameterized queries by default — but HQL injection is still possible with string concatenation
  • Additional defense: input validation, least-privilege DB accounts, WAF rules
java
// ❌ VULNERABLE — string concatenation
String sql = "SELECT * FROM users WHERE name = '" + userInput + "'";

// ✅ SAFE — PreparedStatement
String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, userInput);
ResultSet rs = stmt.executeQuery();

// ✅ SAFE — JPA
@Query("SELECT u FROM User u WHERE u.name = :name")
List<User> findByName(@Param("name") String name);

Hibernate N+1 Problem

Asking 100 questions one-by-one instead of asking all 100 in one meeting. You query the parent (1 query) then lazily load each child (N queries).

  • Symptom: SELECT * FROM orders (1 query) → then N separate SELECT * FROM order_items WHERE order_id = ?
  • Fix 1: JOIN FETCH — @Query("SELECT o FROM Order o JOIN FETCH o.items")
  • Fix 2: @BatchSize(size = 50) — groups lazy loads into batches of 50
  • Fix 3: Entity Graphs — @EntityGraph(attributePaths = {"items"}) on repository methods
  • Detection: enable Hibernate SQL logging (spring.jpa.show-sql=true) and count queries

Interview Tip

Always mention the performance impact: a page listing 100 orders with items could fire 101 database queries without optimization. JOIN FETCH reduces it to 1.

HashMap Internals

  • Backed by an array of Node<K,V> buckets (default capacity 16, load factor 0.75)
  • Hashing: key.hashCode() → spread high bits → bitwise AND with (capacity - 1) to get bucket index
  • Collision handling: linked list (Java 7) → treeify to Red-Black Tree when bucket size > 8 (Java 8+)
  • Resizing: doubles capacity when size > capacity × loadFactor — rehashes all entries (expensive!)
  • Time complexity: O(1) average for get/put, O(log n) worst case (treeified bucket), O(n) worst case (pre-Java 8)

Interview Tip

The 'treeification threshold of 8' is a classic deep-dive question. Explain that it's a space-time trade-off: tree nodes are bigger, so only worth it when the chain is long enough.

Best Time to Buy and Sell Stock (LC 121 & 122)

  • LC 121 (single transaction): track minimum price so far, compute max profit at each step
  • LC 122 (multiple transactions): sum all positive differences — if price goes up, capture that gain
  • Both are O(n) time, O(1) space — single pass through the array
java
// LC 121 — Single Transaction
public int maxProfit(int[] prices) {
    int minPrice = Integer.MAX_VALUE;
    int maxProfit = 0;
    for (int price : prices) {
        minPrice = Math.min(minPrice, price);
        maxProfit = Math.max(maxProfit, price - minPrice);
    }
    return maxProfit;
}

// LC 122 — Multiple Transactions
public int maxProfit2(int[] prices) {
    int profit = 0;
    for (int i = 1; i < prices.length; i++) {
        if (prices[i] > prices[i - 1]) {
            profit += prices[i] - prices[i - 1];
        }
    }
    return profit;
}

Integer to English Words (LC 273)

  • Break the number into chunks of 3 digits (thousands, millions, billions)
  • Each chunk is converted independently using arrays for ones, teens, and tens
  • Handle edge cases: zero, numbers with leading zeroes in chunks, and the teens (11-19) special case
java
private final String[] ones = {"", "One", "Two", "Three", "Four",
    "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven",
    "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen",
    "Seventeen", "Eighteen", "Nineteen"};
private final String[] tens = {"", "", "Twenty", "Thirty", "Forty",
    "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"};
private final String[] thousands = {"", "Thousand", "Million", "Billion"};

public String numberToWords(int num) {
    if (num == 0) return "Zero";
    StringBuilder sb = new StringBuilder();
    int i = 0;
    while (num > 0) {
        if (num % 1000 != 0) {
            sb.insert(0, helper(num % 1000) + thousands[i] + " ");
        }
        num /= 1000;
        i++;
    }
    return sb.toString().trim();
}

Number of Islands (LC 200) — BFS

The "Ripple Effect" — drop a stone in water and watch the ripple expand outward. BFS explores all neighbors level by level.

  • Iterate through the grid. When you find a '1', increment island count and BFS to mark all connected land
  • Use a Queue of coordinates. For each cell, explore 4 directions (up, down, left, right)
  • Mark visited cells as '0' to avoid revisiting — no extra space needed
  • Time: O(m × n), Space: O(min(m, n)) for the queue
java
public int numIslands(char[][] grid) {
    int count = 0;
    for (int i = 0; i < grid.length; i++) {
        for (int j = 0; j < grid[0].length; j++) {
            if (grid[i][j] == '1') {
                count++;
                bfs(grid, i, j);
            }
        }
    }
    return count;
}

private void bfs(char[][] grid, int r, int c) {
    Queue<int[]> queue = new LinkedList<>();
    queue.offer(new int[]{r, c});
    grid[r][c] = '0';
    int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}};
    while (!queue.isEmpty()) {
        int[] cell = queue.poll();
        for (int[] d : dirs) {
            int nr = cell[0] + d[0], nc = cell[1] + d[1];
            if (nr >= 0 && nr < grid.length && nc >= 0 &&
                nc < grid[0].length && grid[nr][nc] == '1') {
                grid[nr][nc] = '0';
                queue.offer(new int[]{nr, nc});
            }
        }
    }
}

Even-Odd Multithreaded Printer

  • Classic thread coordination: two threads take turns printing even/odd numbers
  • Use synchronized + wait() + notify() to alternate between threads
  • Critical: use while-loop (not if) around wait() to guard against spurious wakeups
  • Or use Semaphore or Lock + Condition for a cleaner solution
java
class EvenOddPrinter {
    private int n;
    private volatile boolean isOddTurn = true;

    public synchronized void printOdd() {
        for (int i = 1; i <= n; i += 2) {
            while (!isOddTurn) {  // while, NOT if
                try { wait(); } catch (InterruptedException e) {}
            }
            System.out.println(Thread.currentThread().getName() + ": " + i);
            isOddTurn = false;
            notify();
        }
    }

    public synchronized void printEven() {
        for (int i = 2; i <= n; i += 2) {
            while (isOddTurn) {
                try { wait(); } catch (InterruptedException e) {}
            }
            System.out.println(Thread.currentThread().getName() + ": " + i);
            isOddTurn = true;
            notify();
        }
    }
}

Interview Tip

Always use while(!condition) instead of if(!condition) before wait(). Spurious wakeups are real — the JVM spec allows threads to wake without being notified.

High-Performance Log Parsing (Java 21)

  • Use Files.walk() with parallel streams for scanning directories
  • NIO Channels + BufferedReader for memory-efficient large file reading
  • Pattern matching with regex or String.contains() for filtering log entries
  • Collect results with Collectors.groupingBy() for category-based aggregation
java
// Java 21: High-performance parallel log analysis
public Map<String, Long> countErrorsByType(Path logsDir) throws IOException {
    try (Stream<Path> paths = Files.walk(logsDir)) {
        return paths
            .filter(p -> p.toString().endsWith(".log"))
            .parallel()
            .flatMap(p -> {
                try { return Files.lines(p); }
                catch (IOException e) { return Stream.empty(); }
            })
            .filter(line -> line.contains("ERROR"))
            .collect(Collectors.groupingBy(
                line -> line.split("]")[0].replace("[", "").trim(),
                Collectors.counting()
            ));
    }
}

Curated from a Gemini conversation — structured and formatted for quick reference.