This guide covers the most commonly asked Spring Boot interview questions in 2026 — from basic concepts tested at junior level to advanced architecture questions at senior/lead level. Each answer is written to be concise enough to deliver in an interview while complete enough to actually demonstrate understanding.
The Spring Framework is a comprehensive Java application framework that provides dependency injection, AOP, transaction management and more. It is powerful but requires significant XML or annotation-based configuration to wire everything together.
Spring Boot is built on top of Spring Framework and removes that setup burden by providing:
spring-boot-starter-web) that pull in everything needed for a feature.Auto-configuration is driven by the @EnableAutoConfiguration annotation (included inside @SpringBootApplication). Here is the internal flow:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (Spring Boot 3) — a list of auto-configuration classes.@ConditionalOn* annotations — e.g. @ConditionalOnClass(DataSource.class) only applies if a DataSource class is on the classpath.// Example from DataSourceAutoConfiguration (simplified)
@Configuration
@ConditionalOnClass(DataSource.class)
@ConditionalOnMissingBean(DataSource.class)
public class DataSourceAutoConfiguration {
@Bean
public DataSource dataSource(DataSourceProperties props) {
return props.initializeDataSourceBuilder().build();
}
}
@ConditionalOnMissingBean ensures your own @Bean definition takes precedence — auto-configuration only fires when you have not provided your own.
@SpringBootApplication is a meta-annotation that combines three annotations:
@Configuration — marks the class as a source of Spring bean definitions.@EnableAutoConfiguration — triggers Spring Boot's auto-configuration mechanism.@ComponentScan — scans the current package and sub-packages for components, services, repositories etc.@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
}
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})All four are specialisations of @Component and result in Spring registering the class as a bean. The difference is semantic and enables tooling and additional behaviour:
@Component — generic stereotype. Use when none of the others fit.@Service — marks business logic layer. No extra behaviour; signals intent.@Repository — marks persistence layer. Spring adds automatic exception translation — Spring Data exceptions are translated to DataAccessException hierarchy.@Controller — marks a web MVC controller. Works with @RequestMapping to handle HTTP requests and return views.@RestController — @Controller + @ResponseBody. All handler methods return JSON/XML directly.Starters are pre-packaged dependency descriptors that bundle all the libraries needed for a feature. You add one dependency and Spring Boot pulls in everything automatically.
spring-boot-starter-web — Spring MVC, embedded Tomcat, Jackson for JSON.spring-boot-starter-data-jpa — Spring Data JPA, Hibernate, transaction support.spring-boot-starter-security — Spring Security with sensible defaults.spring-boot-starter-test — JUnit 5, Mockito, AssertJ, Spring Test.spring-boot-starter-actuator — health checks, metrics, info endpoints.spring-boot-starter-validation — Bean Validation (Hibernate Validator).Spring Boot reads configuration from multiple sources in a defined order (higher number = higher priority):
SpringApplicationapplication.properties / application.yml inside the JARapplication-{profile}.properties inside the JARapplication.properties outside the JAR (same directory)-Dmy.prop=value)--my.prop=value)Command-line arguments override everything. This lets you override database URLs or secrets at deploy time without rebuilding.
Profiles let you define environment-specific configuration. You create profile-specific property files:
application-dev.properties # H2 in-memory DB
application-prod.properties # PostgreSQL connection
Activate a profile at startup:
# Via command line
java -jar app.jar --spring.profiles.active=prod
# Via environment variable
SPRING_PROFILES_ACTIVE=prod
You can also annotate beans to only load in certain profiles:
@Bean
@Profile("dev")
public DataSource devDataSource() { ... }
@Value injects individual property values:
@Value("${app.timeout}")
private int timeout;
@ConfigurationProperties maps a group of related properties to a typed POJO — much better for structured config:
@ConfigurationProperties(prefix = "app")
@Component
public class AppProperties {
private int timeout;
private String apiUrl;
// getters and setters
}
Prefer @ConfigurationProperties for anything beyond one or two properties — it gives you type safety, IDE autocompletion and validation with @Validated.
singleton (default) — one instance per Spring container. Shared across all requests.prototype — new instance created every time the bean is requested.request — one instance per HTTP request. Web-aware contexts only.session — one instance per HTTP session.application — one instance per ServletContext.websocket — one instance per WebSocket session.prototype bean into a singleton bean — the prototype only gets created once. Fix with ApplicationContext.getBean() or a scoped proxy.DevTools (spring-boot-devtools) is a development-time dependency that provides:
DevTools is automatically excluded from production JARs — it only activates when running from an exploded build.
@RequestMapping is the generic annotation that maps any HTTP method:
@RequestMapping(value = "/users", method = RequestMethod.GET)
The shorthand annotations are composed meta-annotations that fix the HTTP method:
@GetMapping("/users") — GET requests@PostMapping("/users") — POST requests@PutMapping("/users/{id}") — PUT requests@DeleteMapping("/users/{id}") — DELETE requests@PatchMapping("/users/{id}") — PATCH requestsPrefer the shorthand annotations — they are more readable and intent is explicit.
@PathVariable extracts a value from the URL path itself:
// GET /users/42
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) { ... }
@RequestParam extracts a value from the query string:
// GET /users?page=2&size=10
@GetMapping("/users")
public List<User> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) { ... }
Use @PathVariable for resource identifiers, @RequestParam for filters/pagination/optional params.
@ControllerAdvice (or @RestControllerAdvice) is a class-level annotation that makes exception handler methods apply across all controllers:
@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) {
String message = ex.getBindingResult().getFieldErrors()
.stream().map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return new ErrorResponse("VALIDATION_FAILED", message);
}
}
Add spring-boot-starter-validation, then annotate your DTO fields and add @Valid in the controller:
public class CreateUserRequest {
@NotBlank(message = "Name is required")
private String name;
@Email(message = "Invalid email address")
private String email;
@Min(value = 18, message = "Must be at least 18")
private int age;
}
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest req) {
// if validation fails, MethodArgumentNotValidException is thrown automatically
return ResponseEntity.status(201).body(userService.create(req));
}
ResponseEntity<T> gives you full control over the HTTP response — status code, headers and body:
// Return 201 Created with a Location header
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody CreateUserRequest req) {
User user = userService.create(req);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}").buildAndExpand(user.getId()).toUri();
return ResponseEntity.created(location).body(user);
}
// Return 204 No Content on delete
@DeleteMapping("/users/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
Use it when you need to set a specific status code or add response headers. For simple reads that always return 200, returning the object directly is fine.
Three ways, from simple to full control:
1. Per-method annotation:
@CrossOrigin(origins = "https://myapp.com")
@GetMapping("/users")
public List<User> getUsers() { ... }
2. Global via WebMvcConfigurer (recommended):
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://myapp.com")
.allowedMethods("GET","POST","PUT","DELETE")
.allowCredentials(true);
}
}
3. With Spring Security — configure CORS in the SecurityFilterChain and provide a CorsConfigurationSource bean.
Content negotiation is how Spring decides what format to return (JSON, XML etc.) based on the client's Accept header.
// Client sends: Accept: application/xml
// Spring returns XML if jackson-dataformat-xml is on classpath
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id); // Spring picks JSON or XML automatically
}
You can also force a format via URL extension (deprecated) or request parameter (?format=xml) if configured. Jackson handles JSON by default; add jackson-dataformat-xml for XML support.
@RequestBody — on a method parameter. Tells Spring to deserialise the HTTP request body (JSON/XML) into a Java object using HttpMessageConverter.@ResponseBody — on a method or class. Tells Spring to serialise the return value and write it directly to the HTTP response body instead of resolving a view.@RestController combines @Controller + @ResponseBody, so you don't need @ResponseBody on every method in a REST controller.
JPA is the Java Persistence API specification. Hibernate is the most common JPA implementation. Both require you to write EntityManager code for queries.
Spring Data JPA sits on top and eliminates most of that boilerplate. You define an interface extending JpaRepository and Spring generates the implementation at runtime:
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByEmailAndActiveTrue(String email);
Page<User> findByDepartment(String dept, Pageable pageable);
@Query("SELECT u FROM User u WHERE u.salary > :min")
List<User> findHighEarners(@Param("min") BigDecimal min);
}
Method names like findByEmailAndActiveTrue are parsed by Spring Data to generate the correct JPQL automatically.
@OneToMany and @ManyToMany.@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderLine> lines;
@EntityGraph or a JOIN FETCH query when you know you need the data.The N+1 problem occurs when fetching a list of N entities triggers N additional queries to load a related collection — one query for the list, then one per entity:
// 1 query: SELECT * FROM orders
List<Order> orders = orderRepository.findAll();
// N queries: SELECT * FROM order_lines WHERE order_id = ?
orders.forEach(o -> o.getLines().size()); // N+1 total
Solutions:
JOIN FETCH in JPQL: SELECT o FROM Order o JOIN FETCH o.lines@EntityGraph on the repository method to specify eager loading for that query only.@BatchSize(size = 50) on the collection.@Transactional wraps a method in a database transaction. Spring creates an AOP proxy around the method; the transaction starts before execution and commits on success or rolls back on a runtime exception.
@Service
public class OrderService {
@Transactional
public Order placeOrder(OrderRequest req) {
Order order = orderRepository.save(new Order(req));
inventoryService.deduct(req.getItems()); // same transaction
paymentService.charge(req); // same transaction
return order;
// commits here — all three operations commit together
}
}
Key attributes: propagation (REQUIRED, REQUIRES_NEW etc.), isolation level, rollbackFor, readOnly = true (performance hint for SELECT-only methods).
save() — persists/merges the entity. The SQL is not necessarily executed immediately; Hibernate may batch it and flush at transaction commit.saveAndFlush() — persists and immediately flushes to the database within the current transaction, so the SQL runs right away.Use saveAndFlush() when you need the database-generated ID or need subsequent native queries within the same transaction to see the changes immediately.
Optimistic locking — assumes conflicts are rare. A @Version column is compared on update; if it changed since the entity was read, a OptimisticLockException is thrown. No database lock held.
@Entity
public class Product {
@Version
private Long version; // incremented on every update
}
Pessimistic locking — acquires a database row lock immediately, preventing other transactions from reading or writing. Higher contention, lower throughput:
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Product> findById(Long id);
Use optimistic locking for most cases. Use pessimistic only when conflicts are frequent and a retry is too expensive (e.g. financial deductions).
This exception occurs when you access a lazy-loaded association outside an active Hibernate session (transaction):
// Transaction closes after repository call
User user = userRepository.findById(id).get();
// Session is closed here
user.getOrders().size(); // LazyInitializationException!
Fixes:
@Transactional).JOIN FETCH or @EntityGraph to eagerly load what you need.spring.jpa.open-in-view=true (it hides the problem but causes performance issues).CrudRepository — basic CRUD: save, findById, findAll, delete.PagingAndSortingRepository — extends CrudRepository, adds findAll(Pageable) and findAll(Sort).JpaRepository — extends PagingAndSortingRepository, adds JPA-specific operations: flush, saveAndFlush, deleteInBatch, getById.In most Spring Boot apps you extend JpaRepository — it gives you everything.
Spring Security is a filter chain. Every HTTP request passes through a series of Filter implementations before reaching the servlet:
AuthenticationProvider to validate credentials.Authentication object) is stored here for the duration of the request.FilterSecurityInterceptor checks whether the authenticated user has permission to access the requested resource.You configure the filter chain by defining a SecurityFilterChain bean in Spring Boot 3+.
The flow: client sends credentials → server returns JWT → client sends JWT in Authorization: Bearer header on subsequent requests → server validates the token.
// 1. Filter: extract and validate JWT on every request
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res, FilterChain chain) {
String header = req.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
if (jwtService.isValid(token)) {
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
jwtService.getUsername(token), null,
jwtService.getAuthorities(token));
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(req, res);
}
}
// 2. Register the filter in SecurityFilterChain
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated())
.build();
}
@Secured({"ROLE_ADMIN"}) — simple role-based restriction. Cannot use SpEL expressions.@PreAuthorize("hasRole('ADMIN') and #id == authentication.principal.id") — supports full Spring Expression Language. Can access method arguments (#id), the authentication object, and call custom methods. Much more flexible.@PostAuthorize — evaluates after the method returns; can filter based on the return value.Enable method security with @EnableMethodSecurity on a @Configuration class.
CSRF (Cross-Site Request Forgery) is an attack where a malicious site tricks the user's browser into making authenticated requests to your API using the user's existing session cookie.
Spring Security enables CSRF protection by default for stateful (session-based) apps. It generates a CSRF token that must be included in mutating requests (POST, PUT, DELETE).
For stateless REST APIs using JWT: CSRF is not needed because there are no session cookies. Disable it explicitly:
http.csrf(csrf -> csrf.disable())
OAuth2 is an authorisation framework that allows an application to access resources on behalf of a user without sharing credentials. Common roles: Resource Owner (user), Client (your app), Authorization Server (Google/Okta/Keycloak), Resource Server (your API).
Spring Boot supports OAuth2 via spring-boot-starter-oauth2-resource-server and spring-boot-starter-oauth2-client:
# Validate incoming JWTs from Keycloak
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://keycloak/realms/myrealm
# In SecurityFilterChain:
http.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
The resource server validates the JWT signature and expiry automatically. You access claims via @AuthenticationPrincipal Jwt jwt.
Always use BCryptPasswordEncoder (or Argon2PasswordEncoder for higher security). Never store plain text or MD5/SHA-1 hashed passwords.
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // work factor 12
}
// When registering a user:
user.setPassword(passwordEncoder.encode(rawPassword));
// When authenticating:
passwordEncoder.matches(rawPassword, storedHash); // returns true/false
BCrypt automatically includes a random salt, so the same password produces a different hash every time — making rainbow table attacks useless.
UserDetailsService is a single-method interface Spring Security calls to load a user by username during authentication:
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
return userRepository.findByEmail(username)
.map(u -> User.builder()
.username(u.getEmail())
.password(u.getPassword())
.roles(u.getRoles().toArray(String[]::new))
.build())
.orElseThrow(() -> new UsernameNotFoundException(username));
}
}
Register it as a bean and Spring Security picks it up automatically.
URL-level security in SecurityFilterChain is coarse-grained — it secures endpoints by pattern. Good for broad rules like "all /api/admin/** requires ADMIN role".
Method-level security with @PreAuthorize is fine-grained — it secures individual service methods using SpEL. Good when:
Best practice: use both. URL security as the outer guard, method security as fine-grained business rule enforcement.
@SpringBootTest — loads the full application context. Slow but tests the whole stack end-to-end. Use for integration tests.@WebMvcTest(UserController.class) — loads only the web layer (controllers, filters, security). Service dependencies must be mocked. Fast — good for controller unit tests.@DataJpaTest — loads only JPA-related components (repositories, entity classes). Uses in-memory H2 by default. Rolls back after each test. Good for repository tests.@MockBean — creates a Mockito mock and registers it as a Spring bean, replacing any existing bean of that type.@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void getUser_returnsUser_whenFound() throws Exception {
given(userService.findById(1L))
.willReturn(new UserDTO(1L, "Alice", "alice@example.com"));
mockMvc.perform(get("/api/users/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Alice"))
.andExpect(jsonPath("$.email").value("alice@example.com"));
}
@Test
void getUser_returns404_whenNotFound() throws Exception {
given(userService.findById(99L))
.willThrow(new ResourceNotFoundException("User not found"));
mockMvc.perform(get("/api/users/99"))
.andExpect(status().isNotFound());
}
}
Testcontainers is a Java library that spins up real Docker containers (PostgreSQL, Redis, Kafka etc.) during tests. This means your tests run against the actual database engine, not an in-memory substitute like H2 — catching compatibility issues early.
@SpringBootTest
@Testcontainers
class UserRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void savesUser() {
User user = userRepository.save(new User("Alice"));
assertThat(user.getId()).isNotNull();
}
}
Spring Boot 3.1+ has built-in Testcontainers support via @ServiceConnection, reducing the boilerplate further.
@Mock (Mockito) — creates a plain Mockito mock. No Spring context. Use in pure unit tests with @ExtendWith(MockitoExtension.class).@MockBean (Spring Boot) — creates a Mockito mock and registers it as a Spring bean in the application context, replacing any existing bean of that type. Use in slice tests (@WebMvcTest, @SpringBootTest).@InjectMocks (Mockito) — creates an instance of the class and injects @Mock fields into it. No Spring context needed.When you annotate a test method or class with @Transactional, Spring rolls back the database changes after each test automatically. This means each test starts with a clean state without needing explicit cleanup:
@DataJpaTest
@Transactional // rolls back after each test method
class ProductRepositoryTest {
@Autowired ProductRepository repo;
@Test
void findByCategory_returnsMatchingProducts() {
repo.save(new Product("Widget", "TOOLS"));
List<Product> found = repo.findByCategory("TOOLS");
assertThat(found).hasSize(1);
} // changes rolled back here automatically
}
@DataJpaTest applies @Transactional by default. For @SpringBootTest you add it manually if you want auto-rollback.
@WebMvcTest(UserController.class)
class UserControllerSecurityTest {
@Autowired MockMvc mockMvc;
@MockBean UserService userService;
@MockBean JwtAuthFilter jwtAuthFilter; // mock your custom filter
@Test
@WithMockUser(roles = "ADMIN") // simulate an authenticated user
void adminEndpoint_allowsAdmin() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
@Test
void adminEndpoint_blocks_unauthenticated() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isUnauthorized());
}
}
@WithMockUser populates the SecurityContext for the test. For JWT-based auth, use @WithMockUser or set up a custom SecurityMockMvcRequestPostProcessors.jwt().
The test pyramid recommends more fast, cheap tests at the bottom and fewer slow, expensive tests at the top:
@Mock, no Spring context. Milliseconds each.@WebMvcTest for controllers, @DataJpaTest for repositories. Seconds each.@SpringBootTest with a real database (Testcontainers). Full stack. Tens of seconds each.Keep the pyramid shape: 70% unit, 20% integration, 10% E2E. Inverting it slows your build and makes tests brittle.
Synchronous (HTTP/gRPC):
RestTemplate — older, blocking HTTP client. Still works but being superseded.WebClient — non-blocking, reactive HTTP client. Preferred in Spring Boot 3.@FeignClient (Spring Cloud OpenFeign) — declarative HTTP client. Define an interface and Spring generates the implementation.@FeignClient(name = "inventory-service", url = "${inventory.url}")
public interface InventoryClient {
@GetMapping("/inventory/{sku}")
InventoryResponse checkStock(@PathVariable String sku);
}
Asynchronous (messaging):
A circuit breaker monitors calls to a downstream service. When failures exceed a threshold, it "opens" the circuit and stops calling the service for a cool-down period — returning a fallback instead. This prevents cascading failures.
@Service
public class PaymentService {
@CircuitBreaker(name = "paymentGateway", fallbackMethod = "paymentFallback")
@Retry(name = "paymentGateway")
@TimeLimiter(name = "paymentGateway")
public CompletableFuture<PaymentResult> processPayment(PaymentRequest req) {
return CompletableFuture.supplyAsync(() -> gatewayClient.pay(req));
}
public CompletableFuture<PaymentResult> paymentFallback(
PaymentRequest req, Exception ex) {
return CompletableFuture.completedFuture(
PaymentResult.queued("Payment queued for retry"));
}
}
# application.yml
resilience4j.circuitbreaker.instances.paymentGateway:
failure-rate-threshold: 50
wait-duration-in-open-state: 10s
sliding-window-size: 10
In a microservices environment, instances start and stop dynamically — hardcoding IPs is impossible. Service discovery solves this.
Eureka is a Netflix service registry. Each service registers itself with the Eureka server on startup. Clients look up services by name and get a live instance address.
# Eureka Server
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServer { ... }
# Client (each microservice)
spring.application.name=order-service
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
In Kubernetes environments, Eureka is often replaced by Kubernetes Service DNS, which provides native service discovery without a separate registry.
An API Gateway is a single entry point for all clients. It sits in front of all microservices and handles:
Spring Cloud Gateway is the Spring-native choice (reactive, built on WebFlux). APISIX and Kong are popular open-source alternatives.
When a single user request flows across multiple microservices, distributed tracing lets you follow that request end-to-end, measuring time spent in each service.
Spring Boot 3 uses Micrometer Tracing (replaces Spring Cloud Sleuth). Add the dependency and a tracing backend:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
management.tracing.sampling.probability=1.0
management.zipkin.tracing.endpoint=http://zipkin:9411/api/v2/spans
A trace ID is automatically propagated across HTTP calls and message headers. View the full trace in Zipkin or Jaeger.
Three main approaches:
/actuator/refresh.Secrets (passwords, API keys) should never be in property files or Git. Use Secrets Manager, Vault, or Kubernetes Secrets (ideally sealed with Sealed Secrets).
In microservices, a single business operation (e.g. place order) may span multiple services (Order, Inventory, Payment). Traditional two-phase commit doesn't scale across service boundaries.
The Saga pattern breaks the transaction into a sequence of local transactions, each publishing an event. On failure, compensating transactions undo previous steps.
Two implementations:
Example order flow: OrderCreated → Inventory.reserve → Payment.charge → OrderConfirmed. If payment fails: Payment.compensate → Inventory.release → OrderCancelled.
Option 1: Dockerfile (multi-stage build for small image):
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app
COPY mvnw pom.xml ./
COPY src ./src
RUN ./mvnw package -DskipTests
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]
Option 2: Spring Boot Buildpacks (no Dockerfile needed):
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myapp:latest
Buildpacks produce a production-optimised, layered image automatically.
Add spring-kafka and configure the broker in application.yml:
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=my-group
spring.kafka.consumer.auto-offset-reset=earliest
// Producer
@Service
public class OrderEventProducer {
@Autowired KafkaTemplate<String, OrderEvent> kafka;
public void send(OrderEvent event) {
kafka.send("order-events", event.getOrderId(), event);
}
}
// Consumer
@KafkaListener(topics = "order-events", groupId = "inventory-group")
public void handle(OrderEvent event) {
inventoryService.reserve(event);
}
Actuator adds production-ready monitoring endpoints to your application. Key endpoints:
/actuator/health — application health status (UP/DOWN) including DB, cache, disk space./actuator/metrics — JVM, HTTP request, CPU metrics./actuator/info — custom app metadata (version, build info)./actuator/env — all configuration properties (sensitive — restrict access)./actuator/loggers — view and change log levels at runtime without restart./actuator/threaddump — thread dump for debugging deadlocks.management.endpoints.web.exposure.include=health,metrics,info,loggers
management.endpoint.health.show-details=when-authorized
@Component
public class ExternalApiHealthIndicator implements HealthIndicator {
private final ExternalApiClient client;
@Override
public Health health() {
try {
boolean ok = client.ping();
if (ok) {
return Health.up().withDetail("api", "reachable").build();
}
return Health.down().withDetail("api", "returned error").build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
Spring Boot registers this automatically and includes it in /actuator/health. The overall status is DOWN if any indicator is DOWN.
Add the Micrometer Prometheus registry:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
management.endpoints.web.exposure.include=prometheus,health
management.metrics.export.prometheus.enabled=true
Prometheus scrapes /actuator/prometheus at regular intervals. Grafana connects to Prometheus as a data source and visualises the metrics. Spring Boot auto-exports JVM metrics, HTTP request duration, DB connection pool stats etc. without any extra code.
Enable caching with @EnableCaching, then annotate methods:
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) {
// only called when result is not in cache
return productRepository.findById(id).orElseThrow();
}
@CacheEvict(value = "products", key = "#product.id")
public Product update(Product product) {
return productRepository.save(product);
}
@CachePut(value = "products", key = "#result.id")
public Product create(Product product) {
return productRepository.save(product);
}
}
Default cache is ConcurrentHashMap (in-memory). For distributed caching add Redis:
spring.cache.type=redis
spring.data.redis.host=localhost
Connection pooling maintains a pool of pre-opened database connections. Reusing connections is far cheaper than opening a new TCP connection on every database call.
Spring Boot auto-configures HikariCP (the fastest Java connection pool) when spring-boot-starter-data-jpa is on the classpath.
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000 # 30 seconds
spring.datasource.hikari.idle-timeout=600000 # 10 minutes
spring.datasource.hikari.max-lifetime=1800000 # 30 minutes
Monitor pool usage via /actuator/metrics/hikaricp.connections.active. If active connections stay at max-pool-size, you have a bottleneck.
java \
-Xms512m -Xmx1g \ # heap min/max
-XX:+UseG1GC \ # G1 GC (default Java 9+)
-XX:MaxGCPauseMillis=200 \ # target GC pause
-XX:+HeapDumpOnOutOfMemoryError \ # dump heap on OOM
-XX:HeapDumpPath=/app/heapdump.hprof \
-Dspring.profiles.active=prod \
-jar app.jar
Key tips:
-Xms = -Xmx in containers to avoid heap resizing at runtime.-XX:MaxRAMPercentage=75 instead of fixed heap — lets the JVM adapt to the container memory limit.-Xlog:gc*:file=/app/gc.logSpring MVC is servlet-based and blocking — each request occupies a thread for its entire duration. Under high load with many concurrent I/O-bound requests, threads pile up.
Spring WebFlux is reactive and non-blocking — built on Project Reactor and Netty. A small number of threads handle many concurrent requests because I/O never blocks a thread.
// WebFlux controller
@RestController
public class UserController {
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable Long id) {
return userRepository.findById(id); // Mono = 0 or 1 item
}
@GetMapping("/users")
public Flux<User> getAllUsers() {
return userRepository.findAll(); // Flux = 0..N items
}
}
Use WebFlux when: high concurrency with many slow I/O operations (external API calls, streaming), or you need server-sent events / WebSocket. Stick with MVC when: the team is unfamiliar with reactive programming, or the app is CPU-bound or uses blocking JDBC.
Aspect-Oriented Programming separates cross-cutting concerns (logging, security, transactions, metrics) from business logic by intercepting method calls.
@Aspect
@Component
public class LoggingAspect {
@Around("@annotation(com.example.Audited)")
public Object logExecution(ProceedingJoinPoint pjp) throws Throwable {
String method = pjp.getSignature().getName();
log.info("START {}", method);
long start = System.currentTimeMillis();
try {
Object result = pjp.proceed();
log.info("END {} ({}ms)", method, System.currentTimeMillis()-start);
return result;
} catch (Exception e) {
log.error("FAIL {} — {}", method, e.getMessage());
throw e;
}
}
}
Real use cases: execution time logging, audit trails, retry logic, caching, rate limiting — all without cluttering business code. Spring uses AOP internally for @Transactional, @Cacheable and Spring Security's method-level annotations.
GraalVM Native Image compiles a Spring Boot application ahead-of-time into a native executable — no JVM required at runtime. The result:
Spring Boot 3 has built-in AOT (Ahead-of-Time) processing support. Spring analyses the application at build time and generates optimised code for GraalVM:
# Build native image with Maven
./mvnw -Pnative native:compile
# Or with Buildpacks
./mvnw spring-boot:build-image -Pnative
Trade-offs: longer build times (minutes), dynamic features (reflection, proxies) need explicit hints, and debugging is harder. Ideal for serverless functions and Kubernetes workloads where fast startup and low memory matter.
A checklist for a production-ready, highly available Spring Boot service:
replicas: 3./actuator/health/liveness) and readiness (/actuator/health/readiness) probes.server.shutdown=graceful + spring.lifecycle.timeout-per-shutdown-phase=30s. Drains in-flight requests before stopping.pool_size × instances ≤ db_max_connections).If you have these 60 questions solid, you are well-prepared for most Spring Boot interviews at junior through senior level. For senior/lead roles, go deeper on: