Java Lead Developer Interview Questions & Answers
This guide covers scenario-based questions designed for Lead and Architect level candidates, focusing on system optimization, microservices migration, reliability patterns, and team quality management.
1. How did you optimize the performance of a Java Spring Boot application?
When describing a performance optimization scenario, use the STAR methodology (Situation, Task, Action, Result) with specific metric-driven highlights. Below is a structured response framework.
Database & Persistence Layer Optimizations
- Connection Pool Tuning (HikariCP): Sized the pool based on database core counts and disk capabilities using the formula:
Tuned timeouts (
connectionTimeoutto 2500ms,validationTimeoutto 1000ms, andleakDetectionThresholdto 2000ms) to identify connection leaks early. - Query Tuning & Indexing: Audited slow SQL queries using PostgreSQL pg_stat_statements or MySQL slow query logs. Added composite B-Tree indexes matching filter columns and ordering, and avoided index suppression caused by applying functions to columns (e.g.,
WHERE LOWER(email) = ...instead of functional indexing). - N+1 Query Elimination: Replaced lazy fetching loops with JPQL
JOIN FETCHstatements or Entity Graphs, reducing query roundtrips from database roundtrips to single query. - Write Batching: Enabled Hibernate batch updates (
hibernate.jdbc.batch_size=50andhibernate.order_updates=true) for bulk imports, transforming single-row inserts into batched payloads.
JVM Heap, Garbage Collection & Container Settings
- Container Resource Constraints: Ensured container-awareness by running Java 17+ which respects cgroup limits. Avoided OOMKilled issues due to off-heap overhead (Metaspace, native memory, thread stacks) by keeping the JVM heap at 70-75% of the container memory limits.
- GC Strategy Tuning: Switched from Parallel GC to G1 GC (or ZGC for ultra-low latency requirements) with custom pause targets:
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+ParallelRefProcEnabled
- GC Logging: Configured unified GC logging to monitor promotion rates, allocation rates, and STW (Stop-The-World) pause durations:
-Xlog:gc*,gc+phases=debug:file=/var/log/app-gc.log:time,uptime,pid:filecount=5,filesize=100M
Code & Concurrency Optimizations
- Volatile/Lock Contention Reduction: Overhauled hot path bottlenecks by swapping synchronized blocks for lock-free atomic constructs (e.g.,
AtomicInteger,LongAdderfor counters) and using CAS-basedConcurrentHashMapstructures. - Asynchronous Offloading: Isolated critical user-facing threads by pushing non-blocking jobs (audits, notifications, emails) to custom
ThreadPoolTaskExecutorinstances configured with bounded queues and discard/abort policies.
Metrics & Business Outcomes
- Reduced p99 latency from 1.8s to 120ms.
- Reduced database CPU utilization from 85% to 25%.
- Achieved 30% higher throughput (RPS) on the same container hardware.
2. Scaling a Legacy Monolith to Microservices
Decomposing a monolith requires planning to prevent creating a "distributed monolith" with poor performance and cascading failures.
The Migration Blueprint (Strangler Fig Pattern)
| Migration Component | Role in Strangler Fig | Traffic Allocation | Cutover Strategy |
|---|---|---|---|
| API Gateway / Reverse Proxy | Single ingress interceptor | 100% of external traffic | Intercepts all URI routes; dynamically rewires specific endpoints (e.g. /api/v1/orders/*) away from legacy monolith. |
| Legacy Monolith | Core business logic host | Decreases over time () | Unchanged non-migrated endpoints; code is incrementally deprecated as microservices come online. |
| New Microservice | Extracted bounded context | Increases per release () | Deployed independently with isolated database; receives traffic once green smoke tests pass. |
- API Gateway Interception: Introduce an API Gateway (e.g., Spring Cloud Gateway) in front of the monolith. Initially, 100% of traffic is routed to the legacy monolith.
- Identify Bounded Contexts: Apply Domain-Driven Design (DDD) to identify bounded contexts. Look for domains with different rate-of-change or resource demands (e.g., separating the catalog from the checkout engine).
- Incremental Extraction: Extract one context at a time. The new service is deployed alongside the monolith, and the API gateway is reconfigured to route the specific API prefix (e.g.,
/api/v1/orders/*) to the new service. - Strangling the Monolith: Continue this process until the monolith's core has been completely replaced.
Database Decomposition Strategy
Decomposing databases is the most complex part of migration due to transactions and joins.
- Database-per-Service: Each microservice must own its schema. No service may access another service's tables directly.
- Migration of Tables:
- Create the new microservice's database.
- Implement Dual Writing where the monolith writes to both its old database and the new database (or via a messaging queue).
- Run a background reconciliation tool to backfill legacy data.
- Cut over read queries to the new service.
- Remove the old tables from the monolith's database.
Distributed Transactions & Consistency Patterns
Traditional two-phase commit (2PC) does not scale in microservice environments. Use the following patterns:
- The Saga Pattern: Orchestrates a sequence of local transactions. Each transaction updates database state within a single service and publishes an event. If a step fails, the Saga runner executes compensating transactions to reverse the preceding steps.
- Transactional Outbox Pattern: Guarantees at-least-once delivery of events. Instead of writing to a database and publishing a message to Kafka in the same block (which can fail half-way), write the message to an
OUTBOXtable in the same database transaction. A separate process (Debezium, or a polling publisher) polls the table and publishes to the broker.
| Pipeline Stage | Acting Component | Atomic Operation | Reliability & Delivery Guarantee |
|---|---|---|---|
| 1. Business Write | Application Service | Single ACID Database Transaction | Updates business entity (orders) AND inserts event into outbox table in the same local transaction. Zero distributed 2PC required. |
| 2. Storage | Relational Database (PostgreSQL / MySQL) | Write-Ahead Log (WAL) | Outbox event committed durably to disk. |
| 3. Log Mining (CDC) | Debezium / Poller Agent | Non-blocking transaction log tailing | Reads new records from WAL without adding query load to primary business tables. |
| 4. Broker Publish | Apache Kafka | Producer acks=all delivery | Guaranteed at-least-once event publishing; idempotent consumers handle deduplication via unique event ID. |
3. Enforcing Best Coding Practices in a Team
As a Technical Lead, your role is to scale engineering quality by automating standards rather than relying on manual policing.
Automated Static Analysis & Gates
- SonarQube Quality Gates: Integrated directly into the CI/CD pipeline (e.g., GitHub Actions, GitLab CI). A pull request is blocked from merging if it violates the Quality Gate criteria:
- No new blocker/critical code smells.
- Zero new security vulnerabilities (using SAST scanners).
- Code coverage on new code must exceed 80%.
- Duplicate code block percentage under 3%.
- Checkstyle & Linter Enforcement: Enforce formatting rules during the build process using the
maven-checkstyle-plugin. If formatting does not match the team template (e.g., modified Google Java Style Guide), the compilation fails:mvn clean compile checkstyle:check # Fails build if format is incorrect
Branching & Deployment Workflows
- Trunk-Based Development: Move away from long-lived feature branches (GitFlow) which lead to painful merge conflicts. Developers merge small, short-lived branches to
mainmultiple times a day. - Feature Flags: Use feature flags (e.g., LaunchDarkly, Unleash) to isolate unfinished features in production. This decoupling allows continuous integration without exposing half-baked features to users.
- PR Process Automation: Mandate a template for pull requests containing:
- Link to the ticket/issue.
- Verification steps completed (automated test reports, logs).
- Explicit confirmation of backward compatibility for database schema changes.
- Required approval from at least two senior peers.
4. Debugging: Code Works in "Run" but Fails in "Debug"
This is a classic senior-level debugging question that tests understanding of race conditions and the runtime compiler.
Scenario A: Race Conditions & Thread Timing (Most Common)
Debugging introduces artificial pauses. When a breakpoint is hit, the JVM suspends either the current thread or the entire VM (depending on the debugger configuration).
- The Run Behavior: Two threads execute at full speed, exposing a synchronization bug (e.g., lack of volatile/synchronized, race condition on shared state).
- The Debug Behavior: The thread hitting the breakpoint is paused. This delay allows the other thread to complete its work, resolving the timing collision. The bug disappears while debugging.
- Solution: Avoid breakpoints for timing-dependent code. Instead, use structured thread-safe logging, conditional execution tracing, or thread-safe state assertions.
Scenario B: JIT Tiered Compilation & Optimizations
The JIT compiler optimizes code paths that execute frequently. In run mode, code quickly reaches Level 4 compilation (C2 optimized). In debug mode, compilation levels may be lowered or disabled entirely to keep a 1-to-1 mapping between bytecode instructions and source lines.
- Escape Analysis / Lock Elision: The JIT compiler may optimize away synchronized blocks if it determines an object doesn't escape. In debug mode, this optimization may not occur, altering performance and concurrency characteristics.
- Volatile Visibility: In run mode, the lack of
volatileon a flag can cause a thread to loop infinitely because the JIT caches the value in a CPU register. In debug mode, the register optimizations might be bypassed, making the changes visible and hiding the bug.
How to debug these issues systematically
- Enable thread-safe, non-blocking asynchronous logging (e.g., Logback with
AsyncAppender). - Run diagnostic tools like Thread Dumps (
jstack) or profile the application using async-profiler in run mode. - Configure the debugger to suspend Thread instead of All (VM) to avoid pausing other background tasks and timing loops.
