Skip to main content

Java, Spring Boot & Microservices Interview Questions

This comprehensive guide covers high-level architectural and technical questions frequently asked of Senior Java Developers (5+ years experience).

1. Can you use HashMap in a multi-threaded environment?

It depends on the scenario:

Read-only (safe)

If the map is fully initialized before any thread reads it (e.g., populated at startup, then never modified), a standard HashMap is safe and offers the best performance. This is because there's a happens-before relationship between the initialization thread and the reading threads (via the final field or thread start).

Read-Write (dangerous)

In a concurrent read-write scenario, HashMap is not thread-safe. The risks include:

  • Data corruption: Two threads resizing simultaneously can create an infinite loop in the linked list (pre-Java 8) or corrupt the tree structure (Java 8+).
  • Lost updates: Two threads writing to the same bucket can overwrite each other's entries.
  • ConcurrentModificationException: Only thrown by the iterator's fail-fast mechanism, not guaranteed in all corruption scenarios.

Thread-Safe Alternatives

OptionMechanismRead PerformanceWrite PerformanceBest For
ConcurrentHashMapCAS + node-level synchronized (Java 8+)Excellent (lock-free reads)GoodGeneral concurrent access
Collections.synchronizedMap()Single mutex on entire mapPoor (blocks all readers)PoorLegacy compatibility
HashtableSame as abovePoorPoorNever use (legacy)

ConcurrentHashMap Internal Evolution

Java 7: Used Segment locking (16 segments by default). Each segment was essentially a mini-HashMap with its own lock. Max 16 concurrent writers.

Java 8+: Segments were removed. The new approach uses:

  • CAS (Compare-And-Swap) for inserting into empty buckets (no locking at all)
  • synchronized on the first Node of a non-empty bucket (fine-grained, per-bucket locking)
  • Lock-free reads via volatile node references

This means the concurrency level is equal to the number of buckets, not a fixed 16.

2. String Literal vs. new String()

  • String Literal ("hello"): The JVM checks the String Constant Pool (SCP). If "hello" already exists, it returns the existing reference. If not, it creates a new entry in the pool. Only one object is created (in the pool).
  • new String("hello"): Always creates a new object in the Heap, bypassing the pool check. However, the literal "hello" must still exist in the pool (created at class-loading time if not already present). So up to two objects are created.

Memory Layout

Declaration SyntaxHeap Memory LocationReference Address ExamplePool Reusability (==)
String a = "hello";String Constant Pool (SCP)Pointer 0x100Reuses existing pool instance. a == b returns true.
String b = "hello";String Constant Pool (SCP)Pointer 0x100Shared identical memory reference.
String c = new String("hello");Regular Heap Space (outside SCP)Pointer 0x200 (wraps char[]/byte[])Bypasses pool reuse. a == c returns false, a.equals(c) returns true.
String d = c.intern();String Constant Pool (SCP)Pointer 0x100Pulls existing pool reference. a == d returns true.

Key difference:

String a = "hello";
String b = "hello";
String c = new String("hello");

System.out.println(a == b); // true โ€” same pool reference
System.out.println(a == c); // false โ€” different objects
System.out.println(a.equals(c)); // true โ€” same content

Since Java 7: The String Constant Pool was moved from PermGen to the Heap, allowing it to be garbage collected and to grow dynamically.

3. Java 8 Features & Real-world Usage

Stream API

Enables functional-style processing of collections with a pipeline of lazy intermediate operations and a terminal operation:

List<String> activeEmails = users.stream()
.filter(User::isActive)
.map(User::getEmail)
.filter(email -> email.endsWith("@company.com"))
.sorted()
.collect(Collectors.toList());

Key internal detail: Streams use short-circuit evaluation โ€” operations like findFirst(), limit(), and anyMatch() stop processing as soon as the result is determined, even on infinite streams.

Lambda Expressions

Lambdas are compiled to invokedynamic bytecode instructions (not anonymous inner classes). The JVM generates the implementation class at runtime using LambdaMetafactory, which is more efficient than creating a .class file for each lambda.

Optional

Prevents NullPointerException by explicitly modeling the absence of a value:

// Anti-pattern: DON'T use Optional.get() without check
Optional<User> userOpt = repository.findById(id);

// Good: orElseThrow with meaningful exception
User user = userOpt.orElseThrow(() ->
new UserNotFoundException("User not found: " + id));

// Good: functional transformation chain
String email = userOpt
.map(User::getEmail)
.filter(e -> e.contains("@"))
.orElse("[email protected]");

Performance trap: orElse() always evaluates its argument, even when the Optional has a value. Use orElseGet() for expensive operations:

// BAD: DB call happens even if user exists
User u = optional.orElse(repository.createDefault());

// GOOD: DB call only happens if empty
User u = optional.orElseGet(() -> repository.createDefault());

Default Methods

Added to interfaces to provide backward-compatible API evolution. This is how Java added methods like forEach(), stream(), and spliterator() to the Collection interface in Java 8 without breaking millions of existing implementations.

Diamond problem resolution: If a class implements two interfaces with the same default method, the class must override it to resolve the ambiguity.

4. Spring Boot Starter Dependencies

Spring Boot starters are curated Bill of Materials (BOM) that group related dependencies with pre-verified compatible versions.

How it works internally

  1. You add a single starter (e.g., spring-boot-starter-web)
  2. Maven/Gradle resolves its transitive dependencies (Tomcat, Jackson, Spring MVC, etc.)
  3. Spring Boot's spring-boot-dependencies BOM pins all version numbers to prevent conflicts

Common Starters

StarterIncludes
spring-boot-starter-webSpring MVC, Tomcat, Jackson
spring-boot-starter-data-jpaHibernate, Spring Data JPA, HikariCP
spring-boot-starter-securitySpring Security, BCrypt
spring-boot-starter-testJUnit 5, Mockito, AssertJ

Production tip: Use mvn dependency:tree to audit transitive dependencies and identify version conflicts.

5. What is Spring Boot Actuator?

Actuator provides production-ready operational endpoints to monitor and manage your application at runtime.

Key Endpoints

EndpointPurposeSensitive?
/actuator/healthApplication + dependency health (DB, disk, Redis)No (public by default)
/actuator/metricsJVM memory, GC, HTTP request durations, thread poolsYes
/actuator/envAll configuration properties and their sourcesVery sensitive
/actuator/beansComplete list of Spring beans and their dependenciesYes
/actuator/threaddumpThread dump (equivalent to jstack)Yes
/actuator/heapdumpHeap dump (equivalent to jmap)Extremely sensitive
/actuator/prometheusMetrics in Prometheus scrape formatYes

Security Best Practice

# application.yml โ€” expose only what you need
management:
endpoints:
web:
exposure:
include: health, metrics, prometheus
endpoint:
health:
show-details: when-authorized # Don't expose DB details publicly

Production warning: Never expose /actuator/env or /actuator/heapdump publicly. The env endpoint can leak database passwords, API keys, and secrets. The heapdump can be analyzed offline to extract all in-memory data.

6. Profiles in Spring Boot

Profiles allow environment-specific configuration without code changes.

Configuration hierarchy (highest priority wins)

  1. Command-line arguments (--spring.profiles.active=prod)
  2. JVM system properties (-Dspring.profiles.active=prod)
  3. OS environment variables (SPRING_PROFILES_ACTIVE=prod)
  4. Profile-specific files (application-prod.yml)
  5. Default file (application.yml)

Multi-profile documents (YAML)

# application.yml
spring:
datasource:
url: jdbc:h2:mem:testdb # Default (dev)

---
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:postgresql://prod-db:5432/myapp
hikari:
maximum-pool-size: 20

Profile-specific beans

@Configuration
@Profile("prod")
public class ProdCacheConfig {
@Bean
public CacheManager cacheManager() {
return new RedisCacheManager(/* ... */);
}
}

@Configuration
@Profile("dev")
public class DevCacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager(); // Simple in-memory
}
}

7. API Documentation with Swagger

Swagger (OpenAPI 3.0) generates interactive API documentation from your code annotations.

Setup with SpringDoc (modern replacement for Springfox)

// Just add the dependency โ€” auto-configured
// springdoc-openapi-starter-webmvc-ui

@RestController
@Tag(name = "Users", description = "User management API")
public class UserController {

@Operation(summary = "Get user by ID",
description = "Returns a user if found, 404 otherwise")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "User found"),
@ApiResponse(responseCode = "404", description = "User not found")
})
@GetMapping("/api/users/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
// ...
}
}

Access: Swagger UI is available at /swagger-ui.html and the raw OpenAPI spec at /v3/api-docs.

8. Microservices: Monolithic to Microservices

When decomposing a monolith, follow the Strangler Fig Pattern:

Step-by-step Decomposition

  1. Identify bounded contexts using Domain-Driven Design (DDD) โ€” each context maps to one microservice.
  2. Extract the most painful bottleneck first โ€” not everything at once.
  3. Set up an API Gateway that routes traffic โ€” new requests go to the microservice, legacy routes go to the monolith.
  4. Gradually strangle the monolith by extracting more services over time.

Key Principles

  • Database per Service: Each service owns its data. No shared databases. Use events (Kafka/RabbitMQ) for data synchronization.
  • Single Responsibility: Each service does one thing well and can be deployed independently.
  • Loose Coupling/High Cohesion: Services communicate through well-defined APIs, not shared internal state.

Anti-patterns to avoid

  • Distributed Monolith: Services that must be deployed together are not truly microservices.
  • Shared Database: If two services read/write the same tables, you have a distributed monolith with network overhead.
  • Synchronous chains: A โ†’ B โ†’ C โ†’ D creates brittle coupling. Use async messaging where possible.

9. Fault Tolerance: Resilience4j

Resilience4j (replacement for deprecated Netflix Hystrix) provides fault tolerance patterns:

Circuit Breaker (State Machine)

CLOSED โ”€โ”€(failure rate > threshold)โ”€โ”€โ†’ OPEN
โ†‘ โ”‚
โ”‚ โ”‚ (wait duration)
โ”‚ โ†“
โ””โ”€โ”€(success rate > threshold)โ”€โ”€โ”€โ”€ HALF_OPEN
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
public PaymentResponse processPayment(PaymentRequest request) {
return paymentClient.charge(request);
}

private PaymentResponse paymentFallback(PaymentRequest request, Throwable t) {
log.warn("Payment service unavailable, queuing for retry", t);
retryQueue.enqueue(request);
return PaymentResponse.pending();
}

Configuration

resilience4j:
circuitbreaker:
instances:
paymentService:
failure-rate-threshold: 50 # Open after 50% failures
wait-duration-in-open-state: 30s # Wait 30s before half-open
sliding-window-size: 10 # Evaluate last 10 calls
permitted-number-of-calls-in-half-open-state: 3

Other patterns

  • Retry: Automatic retries with exponential backoff (@Retry)
  • Rate Limiter: Limit calls per time period (@RateLimiter)
  • Bulkhead: Isolate failures by limiting concurrent calls (@Bulkhead)
  • Time Limiter: Cancel calls that take too long (@TimeLimiter)

10. Synchronous vs. Asynchronous Communication

AspectSynchronousAsynchronous
PatternRequest/ResponseFire-and-forget or Event-driven
CouplingTight (caller waits)Loose (caller moves on)
ToolsRestTemplate, WebClient, OpenFeignKafka, RabbitMQ, SQS
Failure impactCascading failures possibleBuffered by message broker
Use caseNeed immediate response (get user details)Notifications, audit logs, data sync

When to use which

  • Sync: When the caller cannot proceed without the response (e.g., checking inventory before placing an order).
  • Async: When the caller doesn't need the result immediately (e.g., sending confirmation email, updating analytics).

The Saga Pattern (Distributed Transactions)

For operations spanning multiple services (e.g., Order โ†’ Payment โ†’ Inventory), use the Saga Pattern with compensating transactions instead of distributed 2PC:

  1. Order Service creates order (PENDING)
  2. Payment Service charges card โ†’ success
  3. Inventory Service reserves stock โ†’ fails
  4. Compensate: Payment Service refunds the charge
  5. Order Service marks order as CANCELLED

11. SQL Joins Summary

Join TypeDescriptionReturns Rows When
Inner JoinReturns only matching rows from both tablesMatch exists in both tables
Left JoinAll rows from left + matching from rightAlways returns left table rows
Right JoinAll rows from right + matching from leftAlways returns right table rows
Full JoinAll rows from both tablesAlways returns all rows
Cross JoinCartesian product (every combination)No join condition (M ร— N rows)
Self JoinTable joined with itselfUsed for hierarchies (employee/manager)

MySQL workaround for Full Join:

SELECT * FROM A LEFT JOIN B ON A.id = B.id
UNION
SELECT * FROM A RIGHT JOIN B ON A.id = B.id;

12. Hibernate: First Level vs. Second Level Cache

FeatureFirst Level CacheSecond Level Cache
ScopePer Session (EntityManager)Per SessionFactory (application-wide)
EnabledAlways (cannot disable)Must be explicitly configured
EvictionWhen session is closed or clearedTTL-based or manual eviction
SharedNo (each session has its own)Yes (all sessions share)
ProviderBuilt into HibernateExternal: EhCache, Hazelcast, Redis
StorageIdentity Map (Map<PK, Entity>)Dehydrated state (serialized form)

How it works

Session session = sessionFactory.openSession();
User u1 = session.get(User.class, 1L); // SQL: SELECT * FROM users WHERE id=1
User u2 = session.get(User.class, 1L); // No SQL โ€” returns from L1 cache
// u1 == u2 โ†’ true (same object reference!)

Session session2 = sessionFactory.openSession();
User u3 = session2.get(User.class, 1L);
// Without L2: executes SQL again
// With L2: reads from L2 cache (but returns a NEW object, u3 != u1)

Query Cache (Often Overlooked)

The entity cache caches by primary key. The Query Cache caches the results of HQL/JPQL queries (storing the list of matching IDs). Both L2 and Query Cache must be enabled for full caching benefit.

@Cacheable // Enable L2 cache for this entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Entity
public class Product { /* ... */ }

๐Ÿ“–
Track Page Progress0 / 635 Read
Knowledge Base Completion0%