25 Backend Interview Questions Revisiting Java Server Fundamentals

 ・ 14 min

photo by Luca Bravo(https://unsplash.com/@lucabravo?utm_source=templater_proxy&utm_medium=referral) on Unsplash

When you revisit interview questions, even concepts you thought you knew well need another pass. Let me walk through traffic handling, concurrency, caching, indexing, and data structures in one go.

Traffic and Performance#

How much traffic have you handled?#

It depends on the service, but it helps to anchor your answer somewhere between hundreds of thousands of daily requests and a few hundred peak RPS. What matters more than the absolute numbers is what bottlenecks you hit at that scale and how you solved them.

Interviewers don't want to hear "we increased the DB connection pool." They want to hear something layered: "I checked the slow query log, added an index, and when that wasn't enough, introduced a cache."

Articles or topics you've studied for handling large-scale traffic#

Here are some go-to resources.

  • Woowahan Brothers tech blog for large-traffic case studies (a Korean food-delivery company with rich content on Java/Spring scaling)
  • Netflix Tech Blog on resilience patterns (Hystrix, Circuit Breaker)
  • Martin Fowler on CQRS and Event Sourcing
  • High Scalability for architecture case studies

The key themes boil down to horizontal scaling, asynchronous processing, layered caching, and the tradeoffs between pessimistic and optimistic locking.

Concurrency and Parallel Processing#

WebFlux vs. ExecutorService#

WebFlux is a non-blocking model based on reactive streams. A small number of event-loop threads handle many requests. ExecutorService, on the other hand, is a thread-pool-based explicit task runner. You hand a unit of work directly to the pool.

Aspect WebFlux ExecutorService
Model Non-blocking / event loop Blocking / thread pool
Best for I/O-bound, long waits CPU-bound, explicit async
Learning curve Steep Gentle
Debugging Hard to follow stack traces Straightforward

WebFlux only pays off when the entire path is non-blocking. One blocking line in the middle can make performance worse.

Where does Virtual Thread fit in?#

Virtual Threads (Project Loom), GA since Java 21 LTS, changed this picture significantly. The point is that you can write plain synchronous code and still get non-blocking-level throughput.

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> userClient.findById(id));
}

A virtual thread isn't pinned 1:1 to an OS thread. When it makes a blocking call, it parks off the carrier thread and resumes later. So OS threads aren't held during I/O waits.

Aspect WebFlux Virtual Thread
Code style Reactive chains (Mono/Flux) Plain synchronous code
Learning curve Steep Almost none
Debugging Hard Normal stack traces
Maturity Long-standing Maturing fast across 21–25 LTS

Virtual threads aren't a silver bullet either. If you call blocking I/O inside a synchronized block, the carrier thread gets pinned, and you lose the benefit. Java 24 relaxed most of these pinning issues, but you still need to audit library compatibility and ThreadLocal usage.

The question "Do I really need WebFlux for a new project?" has a different answer now. If your goal is just multiplexing I/O, virtual threads are often the simpler choice.

Couldn't you just tweak the parallelStream thread setting?#

parallelStream() uses the shared ForkJoinPool (commonPool) by default. You can adjust parallelism via a system property.

System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "8");

The catch is that the common pool is shared across the entire JVM. Heavy work in one place affects others. For isolation, it's safer to create your own ForkJoinPool and run inside it.

ForkJoinPool customPool = new ForkJoinPool(4);
customPool.submit(() -> list.parallelStream().map(...).toList()).get();

parallelStream vs. ExecutorService — when is each more efficient?#

Different jobs.

  • parallelStream: best for CPU-bound transformations over a collection. Works well when tasks are short and uniform.
  • ExecutorService: best when the unit of work is well-defined and you want explicit control over scheduling, results, and cancellation. Suits I/O work or workloads where task durations vary.

In short: data processing pipelines lean toward parallelStream; independent async tasks lean toward ExecutorService.

parallelStream vs. plain stream — which wins?#

Plain streams are single-threaded, so they're faster when data is small, computation is light, or order matters. There's no parallelization overhead (splitting work, merging results).

parallelStream wins only when all of these line up:

  1. Data set is large enough (typically tens of thousands or more)
  2. Computation is heavy (CPU-bound)
  3. No shared state or order dependencies
  4. Splittable data structure (ArrayList, IntStream.range — LinkedList is inefficient)

Throwing parallelStream at a small collection just makes it slower.

Non-Blocking I/O#

Is non-blocking I/O always better?#

No. It depends on the workload. It clearly wins when you have many concurrent connections and long I/O waits. For CPU-bound work, the difference is negligible — sometimes worse.

Performance downsides of non-blocking I/O#

  1. Less context switching, but more code complexity. You have to manage callbacks, reactive chains, and error propagation.
  2. One blocking line can ruin the whole thing. If an event-loop thread blocks, every request stalls.
  3. Debugging and stack traces are painful. Async boundaries break the call stack.
  4. No win for CPU-bound work. More threads can't beat the core count.

Caching#

Cache experience#

Common tools: Redis, Caffeine, Ehcache. Typical use cases:

  • Caching responses for read-heavy query APIs
  • Caching external API call results
  • Session/auth token stores
  • Distributed locks

When does the cache get refreshed?#

Depends on the strategy.

  • TTL expiration: simplest — invalidate after a time window.
  • Write-Through: write to DB and cache simultaneously.
  • Write-Behind: write to cache first, flush to DB asynchronously.
  • Cache-Aside (Lazy Loading): on a cache miss, read from DB and populate. On update, invalidate or refresh.
  • Event-driven invalidation: refresh from DB change events (Kafka, CDC).

How can cache–DB consistency break?#

It can. Common scenarios:

  1. In the brief gap between DB update and cache invalidation, another request reads the old value and refills the cache.
  2. In a multi-instance setup, one node refreshes while another reads stale data.
  3. A transaction rolls back, but the cache was already updated.
  4. During TTL, the DB changes, and stale data stays visible.

Mitigations: invalidate the cache after the DB transaction commits, use a distributed lock to serialize updates, or use a version key to ignore stale entries.

JVM and Monitoring#

How to prevent OOM in a Java application#

For prevention:

  • Tune heap size and GC algorithm for your workload (-Xmx, -Xms, -XX:+UseG1GC, etc.)
  • Avoid memory leaks: don't pile into static collections forever, clean up ThreadLocals, cap cache sizes
  • Stream large data: don't load entire files or DB results into memory
  • Avoid resource leaks: use try-with-resources

GC choice also depends on workload.

  • G1GC: solid default for most server workloads. Balanced throughput/latency.
  • ZGC: production-ready since Java 15. Targets sub-millisecond pauses even on tens of GB to TB heaps. Java 21 introduced Generational ZGC (-XX:+UseZGC -XX:+ZGenerational), closing the throughput gap.
  • Shenandoah: another low-latency OpenJDK GC with similar goals.
  • Parallel GC: for batch jobs where throughput matters more than latency.

For latency-sensitive, high-traffic services, more teams are moving from G1 to Generational ZGC.

When OOM does happen:

  1. Use -XX:+HeapDumpOnOutOfMemoryError to auto-save a heap dump.
  2. Analyze leaks with MAT (Memory Analyzer Tool).
  3. Read GC logs for patterns (steady growth → leak; sudden spike → traffic pattern).
  4. Bump the heap as a stopgap, but fix the leak as the real solution.

How did you monitor servers?#

These days OpenTelemetry (OTel) is effectively the standard. You emit metrics, logs, and traces through one SDK/protocol (OTLP), and pick the backend separately.

By layer:

  • Collection (SDK / agent): OpenTelemetry Java Agent, Micrometer Tracing
  • Metrics backend: Prometheus + Grafana, CloudWatch
  • Logs backend: Loki, Elasticsearch, OpenSearch
  • Traces backend: Grafana Tempo, Jaeger
  • Commercial APM: DataDog, New Relic, Grafana Cloud (all support OTLP ingest)
  • Korean tools: Pinpoint and Scouter are still around, but new systems trend toward OTel
  • Alerting: Grafana Alert, PagerDuty, Slack integrations

The real goal is to see traffic, error rate, latency, and resource use (USE/RED metrics) on one dashboard, with traceId stitching logs, metrics, and traces together.

synchronized and Atomic Operations#

Explain synchronized#

synchronized is Java's monitor-lock-based synchronization keyword. Apply it to a method or block, and only the thread holding the same monitor can enter.

public synchronized void increment() {
    count++;
}

Internally, lock state is recorded in the object header's Mark Word, and the JVM escalates from biased lock → lightweight lock → heavyweight lock based on contention.

Problems with synchronized#

  1. Performance overhead: under heavy contention it escalates to OS-level (heavyweight) locks, increasing context switches.
  2. Deadlock risk: tangle the lock acquisition order and you're stuck.
  3. Reentrant but not interruptible: Lock offers lockInterruptibly(); synchronized doesn't.
  4. No fairness control: no guarantee about which thread gets the lock next.
  5. No timeout: it can wait forever.

Alternatives like ReentrantLock, ReadWriteLock, and StampedLock give you finer control.

Do you know AtomicInteger?#

AtomicInteger is a lock-free integer class built on CAS (Compare-And-Swap). Internally it calls Unsafe.compareAndSwapInt (or VarHandle on newer versions).

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();

It uses an atomic CPU instruction instead of taking a lock, which makes it ideal for lightweight counters.

How would you implement AtomicInteger yourself?#

The core idea is a CAS loop.

public class MyAtomicInteger {
    private volatile int value;
    private static final VarHandle VALUE;
 
    static {
        try {
            VALUE = MethodHandles.lookup()
                .findVarHandle(MyAtomicInteger.class, "value", int.class);
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }
 
    public int incrementAndGet() {
        int prev, next;
        do {
            prev = value;
            next = prev + 1;
        } while (!VALUE.compareAndSet(this, prev, next));
        return next;
    }
}

The recipe: volatile for visibility + CAS for atomicity + retry on failure.

When to use AtomicInteger and when not to#

Good fits:

  • Counting or flags on a single variable
  • Low-contention environments

Avoid in these cases:

  • Heavy contention: CAS retries explode and performance tanks. Reach for LongAdder, which spreads work across cells. Internally, the JDK marks those cells with @jdk.internal.vm.annotation.Contended to prevent false sharing (performance loss from sharing cache lines).
  • Multi-variable consistency: locks or transactions are a better fit.

Data Structures: HashMap and HashSet#

When to use HashSet and HashMap#

When you need average O(1) lookup/insert. HashMap is a key-value map; HashSet is a duplicate-free set. Internally, HashSet wraps HashMap.

When they don't fit:

  • Need ordering: LinkedHashMap, LinkedHashSet
  • Need sorting: TreeMap, TreeSet (O(log n))
  • Need concurrency: ConcurrentHashMap
  • No-null requirement: Hashtable and ConcurrentHashMap reject nulls

When HashSet doesn't perform as expected#

  1. hashCode() is poorly distributed — collisions push you toward O(n).
  2. equals() and hashCode() are inconsistent — you can't find what you put in.
  3. Mutable keys with mutated fields — the hash drifts, lookups fail.
  4. Initial capacity is too low — frequent resizing (rehash).
  5. Load factor is too high — more collisions.

Since Java 8, when a bucket's collisions exceed a threshold, it converts to a Red-Black Tree for O(log n). But fundamentally, a good hashCode() matters most.

ThreadLocal#

Have you used ThreadLocal in Java?#

Yes — typical uses:

  • Per-request context: auth info, transaction IDs, MDC (log context)
  • Reusing non-thread-safe objects like SimpleDateFormat
  • Spring's RequestContextHolder and TransactionSynchronizationManager rely on ThreadLocal internally

The catch: always call remove() to clean up. In thread-pool environments, threads are reused, so a leftover ThreadLocal can carry into the next request — leading to security bugs or memory leaks.

MySQL Indexes#

What is a MySQL index and when should you use one?#

An index is a separate data structure that speeds up access to table data. Like a book's table of contents, it helps you find the right rows fast.

Good places to add an index:

  • Columns frequently used in WHERE
  • JOIN keys
  • ORDER BY / GROUP BY columns
  • High-cardinality columns (many distinct values)

MySQL 8.0 added more index options:

  • Descending Index: handle descending sort at the index level
  • Invisible Index: hide from the optimizer to safely test impact
  • Functional Index: index expressions like JSON_EXTRACT(...) or LOWER(col)
  • Multi-Valued Index (JSON arrays): index values inside a JSON array

Are more indexes always better?#

No. Indexes raise write costs. Every INSERT, UPDATE, and DELETE has to update every index. They also use disk space, and the optimizer might pick the wrong one.

The rule: only the minimum needed for actual query patterns.

B+ Tree as the index data structure#

InnoDB uses a B+ Tree. Key properties:

  • Actual data (or PK) lives only in leaf nodes
  • Leaf nodes form a doubly-linked list, making range scans fast
  • Balanced tree — every leaf is at the same depth
  • Optimized for disk I/O page size — one I/O reads many keys

InnoDB's PK index is a clustered index, so the leaves store the actual rows. Secondary indexes store PK values in their leaves and then do a second lookup against the clustered index (called a back-to-base or lookup).

charAt Type Issues#

Cases where charAt's return type vs. receiving type can cause bugs#

String.charAt(int) returns a char. But Java treats char as an unsigned 16-bit integer, so it auto-promotes to int and you can get unexpected results.

String s = "1";
int x = s.charAt(0);    // 49 (ASCII for '1')
int y = s.charAt(0) - '0'; // 1

If you just add s.charAt(0), you get 49, not 1. Comparisons trip people up too.

char c = '1';
if (c == 1) { ... }    // false (49 != 1)
if (c == '1') { ... }  // true

Another big trap: Unicode supplementary characters (surrogate pairs). Emojis and some CJK characters are encoded as two chars, so a single charAt(i) won't return the full character.

String s = "𝕏"; // U+1D54F
s.length();    // 2
s.charAt(0);   // returns half a surrogate
s.codePointAt(0); // 119887, the full code point

For character-level work, use codePointAt, String.codePoints(), or Character.toChars().

Wrap-up#

Interview questions aren't about memorizing answers — they're about explaining the tradeoffs in real situations. The same question gets a much deeper answer when you can cover both "why does it work this way?" and "when would I pick something else?"

If any of these 25 questions felt fuzzy, run a small example in code to lock it in.


Difficulties increase the nearer we get to the goal.

— Johann Wolfgang von Goethe


Other posts
Government Grants and Investment — What Every Founder Should Know 커버 이미지
 ・ 5 min

Government Grants and Investment — What Every Founder Should Know

Core Backend Concepts for Interview Preparation 커버 이미지
 ・ 11 min

Core Backend Concepts for Interview Preparation

Common Backend Engineering Interview Questions 커버 이미지
 ・ 27 min

Common Backend Engineering Interview Questions