Senior Java developer interviews go beyond basic Spring Boot usage. Interviewers expect you to understand how Spring Boot works under the hood, architect production-ready systems, and make informed decisions about reactive vs imperative programming, microservices patterns, and performance optimization.
This guide covers Spring Boot 4.1.1, Spring Framework 7.0.9, and the Spring Cloud 2025.1 line as of September 2026. It focuses on internals, custom starters, Spring Cloud, WebFlux, and production architecture decisions. Check the compatibility matrix before combining a Boot release with a Spring Cloud release train.
Table of Contents
- Auto-Configuration Questions
- Conditional Annotation Questions
- Bean Lifecycle Questions
- Custom Starter Questions
- Configuration Properties Questions
- Spring Cloud Config Questions
- Service Discovery Questions
- API Gateway Questions
- Distributed Tracing Questions
- WebFlux Questions
- Production Actuator Questions
- Performance Tuning Questions
Auto-Configuration Questions
Understanding how Spring Boot works enables better debugging and custom solutions.
How does Spring Boot auto-configuration actually work?
Spring Boot auto-configuration is the mechanism that automatically configures beans based on classpath dependencies and existing bean definitions. When you add @SpringBootApplication to your main class, it implicitly includes @EnableAutoConfiguration, which triggers the entire auto-configuration process. The AutoConfigurationImportSelector loads candidate configuration classes from META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (or spring.factories in older versions).
Each auto-configuration class is evaluated against conditional annotations to determine if it should be activated. This conditional evaluation happens at startup, checking classpath contents, existing beans, and property values. The key insight is that auto-configuration provides sensible defaults that back off when you define your own beans.
flowchart TB
subgraph BOOT["@SpringBootApplication"]
ENABLE["@EnableAutoConfiguration"]
IMPORT["@Import(AutoConfigurationImportSelector.class)"]
ENABLE --> IMPORT
end
subgraph SELECTOR["AutoConfigurationImportSelector"]
S1["1. Load META-INF/spring/...AutoConfiguration.imports"]
S2["2. Filter by @Conditional annotations"]
S3["3. Order by @AutoConfigureOrder, Before, After"]
S4["4. Return matching configuration classes"]
S1 --> S2 --> S3 --> S4
end
subgraph CONDITIONAL["Conditional Evaluation"]
C1["@ConditionalOnClass<br/>Class exists on classpath?"]
C2["@ConditionalOnMissingBean<br/>Bean not already defined?"]
C3["@ConditionalOnProperty<br/>Property set to expected value?"]
C4["@ConditionalOnWebApplication<br/>Web app context?"]
end
BOOT --> SELECTOR
SELECTOR --> CONDITIONALWhat does a real auto-configuration class look like?
Auto-configuration classes combine multiple conditional annotations to create sophisticated activation logic. The DataSourceAutoConfiguration class demonstrates this pattern well—it checks for required classes on the classpath, verifies that the user hasn't defined their own DataSource bean, and delegates to nested configuration classes for embedded versus pooled data sources.
The proxyBeanMethods = false setting uses "lite" @Configuration processing without a CGLIB subclass. It is correct when one @Bean method does not rely on direct calls to another method being intercepted; use it for semantics first, not as a promised performance gain.
// DataSourceAutoConfiguration (simplified)
@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration {
}
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class,
DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class })
protected static class PooledDataSourceConfiguration {
}
}Conditional Annotation Questions
Conditional annotations control when beans and configurations are activated.
What are the built-in @Conditional annotations in Spring Boot?
Spring Boot provides a rich set of conditional annotations that evaluate various aspects of the application context. These annotations can be combined on the same class or method—all conditions must match for the bean to be created. Understanding these conditions is essential for debugging why certain auto-configurations activate or don't activate.
The most commonly used conditions check for class presence (@ConditionalOnClass), bean absence (@ConditionalOnMissingBean), and property values (@ConditionalOnProperty). Less common but equally useful are resource conditions, expression conditions, and web application type conditions.
// Built-in conditions
@ConditionalOnClass(DataSource.class) // Class on classpath
@ConditionalOnMissingClass("com.example.Foo") // Class NOT on classpath
@ConditionalOnBean(DataSource.class) // Bean exists
@ConditionalOnMissingBean(DataSource.class) // Bean doesn't exist
@ConditionalOnProperty( // Property matches
prefix = "app.feature",
name = "enabled",
havingValue = "true",
matchIfMissing = false
)
@ConditionalOnResource(resources = "classpath:schema.sql")
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnExpression("${app.advanced:false} and ${app.experimental:false}")How do you create a custom condition?
Custom conditions implement the Condition interface and provide logic for determining whether a configuration should activate. This is useful when built-in conditions do not cover a local, deterministic requirement. Do not make network calls or check live service availability from a condition: condition evaluation happens while the context is being built and should not turn startup into a remote health check.
The ConditionContext provides access to the environment, bean factory, class loader, and resource loader, giving you full flexibility in what you can evaluate.
// Custom condition
public class OnProductionEnvironmentCondition implements Condition {
@Override
public boolean matches(ConditionContext context,
AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
String[] activeProfiles = env.getActiveProfiles();
return Arrays.asList(activeProfiles).contains("production");
}
}
@Configuration
@Conditional(OnProductionEnvironmentCondition.class)
public class ProductionOnlyConfiguration {
// Only loaded in production
}Bean Lifecycle Questions
Understanding the bean lifecycle helps with initialization logic and debugging.
What is the complete Spring bean lifecycle?
The Spring bean lifecycle is a pipeline with extension points rather than exactly eight universal phases. After instantiation and property population, relevant Aware callbacks run. BeanPostProcessor implementations surround initialization; one of them invokes @PostConstruct, followed by InitializingBean.afterPropertiesSet() and a custom init method where configured.
AOP infrastructure can return a proxy from post-processing, but not every bean is proxied and other post-processors may act at different points. Destruction callbacks run for eligible beans managed by a closing context; prototype-scoped bean destruction is not managed automatically.
flowchart TB
P1["1. Instantiation<br/>• Constructor called<br/>• Dependencies injected"]
P2["2. Population<br/>• @Autowired fields/setters<br/>• @Value properties resolved"]
P3["3. Aware Interfaces<br/>• BeanNameAware<br/>• BeanFactoryAware<br/>• ApplicationContextAware"]
P4["4. Pre-Initialization<br/>• BeanPostProcessor.postProcessBefore<br/>• @PostConstruct methods"]
P5["5. Initialization<br/>• InitializingBean.afterPropertiesSet<br/>• Custom init-method"]
P6["6. Post-Initialization<br/>• BeanPostProcessor.postProcessAfter<br/>• AOP proxies created"]
P7["7. Ready for Use"]
P8["8. Destruction<br/>• @PreDestroy methods<br/>• DisposableBean.destroy<br/>• Custom destroy-method"]
P1 --> P2 --> P3 --> P4 --> P5 --> P6 --> P7 --> P8How do you implement lifecycle callbacks in a Spring bean?
You can implement lifecycle callbacks through annotations (@PostConstruct, @PreDestroy), interfaces (InitializingBean, DisposableBean), or Aware interfaces for accessing framework components. The execution order is predictable: Aware methods run first, then @PostConstruct, then afterPropertiesSet(). For destruction, @PreDestroy runs before destroy().
Choose annotations, interfaces, or explicit @Bean(initMethod=..., destroyMethod=...) based on ownership and coupling. The callback order is defined, but implementing a Spring interface is not required to obtain it.
@Component
public class LifecycleDemoBean implements BeanNameAware, InitializingBean,
DisposableBean, ApplicationContextAware {
private String beanName;
private ApplicationContext context;
public LifecycleDemoBean() {
System.out.println("1. Constructor");
}
@Autowired
public void setDependency(SomeDependency dep) {
System.out.println("2. Dependency injection");
}
@Override
public void setBeanName(String name) {
this.beanName = name;
System.out.println("3. BeanNameAware: " + name);
}
@Override
public void setApplicationContext(ApplicationContext ctx) {
this.context = ctx;
System.out.println("3. ApplicationContextAware");
}
@PostConstruct
public void postConstruct() {
System.out.println("4. @PostConstruct");
}
@Override
public void afterPropertiesSet() {
System.out.println("5. InitializingBean.afterPropertiesSet");
}
@PreDestroy
public void preDestroy() {
System.out.println("8. @PreDestroy");
}
@Override
public void destroy() {
System.out.println("8. DisposableBean.destroy");
}
}Custom Starter Questions
Creating custom starters is a senior-level skill for building reusable infrastructure.
When should you create a custom Spring Boot starter?
Custom starters are appropriate when you have configuration that needs to be reused across multiple projects. Common scenarios include company-wide standards for logging, security, and observability; integrations with internal APIs or proprietary databases; and infrastructure setup for messaging or caching systems. A starter bundles auto-configuration, dependencies, and sensible defaults into a single dependency that other projects can include.
A common structure uses an autoconfigure module for configuration logic and a small starter module for dependencies. The split is useful when consumers may want the auto-configuration without the starter's dependency choices; a focused internal starter can also use one module.
my-company-spring-boot-starter/
├── my-company-spring-boot-autoconfigure/ # Auto-configuration module
│ ├── src/main/java/
│ │ └── com/company/autoconfigure/
│ │ ├── MyServiceAutoConfiguration.java
│ │ ├── MyServiceProperties.java
│ │ └── MyService.java
│ ├── src/main/resources/
│ │ └── META-INF/
│ │ └── spring/
│ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ └── pom.xml
│
└── my-company-spring-boot-starter/ # Starter module (dependencies only)
└── pom.xml
How do you build an auto-configuration class for a custom starter?
Building an auto-configuration requires three components: configuration properties that bind external configuration, the service class being configured, and the auto-configuration class that wires everything together. The auto-configuration class uses conditional annotations to ensure it only activates when appropriate—typically when the service class is on the classpath, the feature is enabled, and the user hasn't defined their own bean.
The @ConditionalOnMissingBean annotation is particularly important—it allows users to override your default configuration by defining their own bean of the same type.
// 1. Configuration properties
@ConfigurationProperties(prefix = "company.service")
public class MyServiceProperties {
private boolean enabled = true;
private String endpoint = "https://api.company.com";
private Duration timeout = Duration.ofSeconds(30);
private RetryConfig retry = new RetryConfig();
// Nested configuration
public static class RetryConfig {
private int maxAttempts = 3;
private Duration backoff = Duration.ofMillis(100);
// Getters and setters
}
// Getters and setters
}
// 2. The service being auto-configured
public class MyService {
private final MyServiceProperties properties;
private final RestClient restClient;
public MyService(MyServiceProperties properties, RestClient restClient) {
this.properties = properties;
this.restClient = restClient;
}
public Response callApi(Request request) {
return restClient.post()
.uri(properties.getEndpoint())
.body(request)
.retrieve()
.body(Response.class);
}
}
// 3. Auto-configuration class
@AutoConfiguration
@ConditionalOnClass(MyService.class)
@ConditionalOnProperty(
prefix = "company.service",
name = "enabled",
havingValue = "true",
matchIfMissing = true
)
@EnableConfigurationProperties(MyServiceProperties.class)
public class MyServiceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService(MyServiceProperties properties,
ObjectProvider<RestClient.Builder> restClientBuilder) {
RestClient restClient = restClientBuilder
.getIfAvailable(RestClient::builder)
.requestFactory(new JdkClientHttpRequestFactory())
.build();
return new MyService(properties, restClient);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "company.service.retry", name = "enabled",
havingValue = "true", matchIfMissing = true)
public RetryTemplate myServiceRetryTemplate(MyServiceProperties properties) {
return RetryTemplate.builder()
.maxAttempts(properties.getRetry().getMaxAttempts())
.exponentialBackoff(
properties.getRetry().getBackoff().toMillis(),
2.0,
30000)
.build();
}
}How do you register an auto-configuration class?
Auto-configuration classes for current Spring Boot releases are registered in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. Each line contains one fully qualified auto-configuration class name.
The starter POM pulls in the autoconfigure module and any required runtime dependencies, making it a single dependency for consuming projects.
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.company.autoconfigure.MyServiceAutoConfiguration
<!-- my-company-spring-boot-starter/pom.xml -->
<project>
<artifactId>my-company-spring-boot-starter</artifactId>
<dependencies>
<!-- Pull in the autoconfigure module -->
<dependency>
<groupId>com.company</groupId>
<artifactId>my-company-spring-boot-autoconfigure</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Required dependencies for users -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>How do you provide IDE auto-completion for custom configuration properties?
Configuration metadata enables IDE auto-completion and documentation for your custom properties. Add the spring-boot-configuration-processor annotation processor dependency, and document properties using Javadoc comments. For additional hints like valid values or deprecation warnings, create an additional-spring-configuration-metadata.json file.
This metadata improves the developer experience significantly, making your starter feel as polished as official Spring Boot starters.
// Enable IDE auto-completion for properties
// Add spring-boot-configuration-processor dependency
@ConfigurationProperties(prefix = "company.service")
public class MyServiceProperties {
/**
* Enable or disable the company service integration.
*/
private boolean enabled = true;
/**
* Base URL for the company API.
*/
private String endpoint = "https://api.company.com";
}// META-INF/additional-spring-configuration-metadata.json
{
"properties": [
{
"name": "company.service.endpoint",
"type": "java.lang.String",
"description": "Base URL for the company API.",
"defaultValue": "https://api.company.com"
}
],
"hints": [
{
"name": "company.service.endpoint",
"values": [
{
"value": "https://api.company.com",
"description": "Production endpoint"
},
{
"value": "https://sandbox.company.com",
"description": "Sandbox endpoint"
}
]
}
]
}Configuration Properties Questions
Advanced configuration management is essential for production applications.
What is the order of property source precedence in Spring Boot?
Spring Boot uses an ordered list of property sources in which a later source overrides an earlier one. The full order also includes test and Devtools sources, so command-line arguments are not universally the highest-precedence source. In a normal packaged run, a useful simplified order from lower to higher precedence is: default properties, @PropertySource, config data, random values, environment variables, JVM system properties, JNDI, Servlet context/config parameters, SPRING_APPLICATION_JSON, and command-line arguments.
Config data has its own ordering: packaged defaults, packaged profile-specific files, external defaults, then external profile-specific files. Prefer one configuration format per location, use environment/config trees for deployment input, and consult the current reference when precedence itself matters.
flowchart TB
subgraph PRECEDENCE["Common runtime property sources (lower to higher)"]
P1["Default properties"]
P2["@PropertySource"]
P3["Config data<br/>packaged then external"]
P4["OS environment variables"]
P5["Java system properties"]
P6["JNDI and Servlet init parameters"]
P7["SPRING_APPLICATION_JSON"]
P8["Command-line arguments"]
end
P1 --> P2 --> P3 --> P4 --> P5 --> P6 --> P7 --> P8How do you use profile-based configuration?
Profile-based configuration allows different settings for different environments. You can create separate files like application-local.yml and application-production.yml, or use document separators within a single file. Beans can be conditionally created using @Profile annotations, and profiles can be activated programmatically based on runtime conditions.
The spring.config.activate.on-profile property in YAML indicates which profile a configuration section belongs to. Multiple profiles can be active simultaneously, with later profiles overriding earlier ones.
# application.yml - Common settings
spring:
application:
name: my-service
---
# Profile-specific document in application.yml
spring:
config:
activate:
on-profile: local
datasource:
url: jdbc:h2:mem:testdb
logging:
level:
com.company: DEBUG
---
# Another profile-specific document in application.yml
spring:
config:
activate:
on-profile: production
datasource:
url: jdbc:postgresql://prod-db:5432/myapp
hikari:
maximum-pool-size: 20
logging:
level:
root: WARN
com.company: INFO// Profile-specific beans
@Configuration
public class DataSourceConfig {
@Bean
@Profile("local")
public DataSource h2DataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
@Bean
@Profile("production")
public DataSource productionDataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
}
// Programmatic profile activation
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
if (System.getenv("KUBERNETES_SERVICE_HOST") != null) {
app.setAdditionalProfiles("kubernetes");
}
app.run(args);
}
}How do you create type-safe configuration with validation?
Type-safe configuration uses @ConfigurationProperties to bind properties to strongly-typed Java objects. Adding @Validated enables JSR-303 validation annotations like @NotNull, @Min, and @Max. Nested configuration classes organize related properties, and @DurationUnit specifies the time unit for Duration properties.
This approach catches configuration errors at startup rather than runtime, provides IDE auto-completion, and makes configuration usage self-documenting through the class structure.
@ConfigurationProperties(prefix = "app.features")
@Validated
public class FeatureProperties {
@NotNull
private Map<String, FeatureFlag> flags = new HashMap<>();
@Valid
private RateLimiting rateLimiting = new RateLimiting();
public static class FeatureFlag {
private boolean enabled = false;
private Set<String> allowedUsers = new HashSet<>();
private LocalDateTime enabledUntil;
// Getters and setters
}
public static class RateLimiting {
@Min(1)
@Max(10000)
private int requestsPerMinute = 100;
@DurationUnit(ChronoUnit.SECONDS)
private Duration window = Duration.ofMinutes(1);
// Getters and setters
}
}
// Usage
@Service
@RequiredArgsConstructor
public class FeatureService {
private final FeatureProperties features;
public boolean isFeatureEnabled(String featureName, String userId) {
FeatureFlag flag = features.getFlags().get(featureName);
if (flag == null || !flag.isEnabled()) {
return false;
}
if (!flag.getAllowedUsers().isEmpty() &&
!flag.getAllowedUsers().contains(userId)) {
return false;
}
if (flag.getEnabledUntil() != null &&
LocalDateTime.now().isAfter(flag.getEnabledUntil())) {
return false;
}
return true;
}
}Spring Cloud Config Questions
Spring Cloud provides tools for distributed systems patterns.
How do you set up Spring Cloud Config Server?
Spring Cloud Config Server provides centralized configuration management for microservices. The server can be backed by Git repositories, HashiCorp Vault, or databases, serving configuration to client applications over HTTP. Git-backed configuration provides versioning, audit trails, and the ability to use branches for different environments.
Config Server supports encrypted values, but decrypting a repository value and returning it to every authorized client is not the same as a dedicated secret manager with narrowly scoped identity, rotation, and audit controls. The search-paths configuration supports placeholders like {application} for organizing configurations by service name.
# Config Server application.yml
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: https://github.com/company/config-repo
search-paths: '{application}'
default-label: main
encrypt:
enabled: true
encrypt:
key: ${ENCRYPT_KEY} # Or use keystore for asymmetric
server:
port: 8888# Client application.yml
spring:
application:
name: order-service
config:
import: "configserver:http://config-server:8888"
cloud:
config:
fail-fast: true
retry:
max-attempts: 6
initial-interval: 1000
multiplier: 1.5How do you refresh configuration at runtime without restarting?
The @RefreshScope annotation marks beans that can be recreated when configuration changes. A refresh event clears scoped instances, and the next method call obtains a newly initialized target. For cluster-wide refresh, Spring Cloud Bus can broadcast events through a broker. The refresh endpoints must be explicitly exposed and secured.
Refresh is not an atomic transaction across instances: during rollout, callers can observe different values and a partial failure can leave a mixed fleet. Use versioned, backward-compatible values and observability; avoid refresh-scoping stateful resources unless their lifecycle is understood.
// Refresh configuration at runtime
@RefreshScope
@Service
public class PricingService {
@Value("${pricing.discount-percentage:0}")
private double discountPercentage;
public BigDecimal calculatePrice(BigDecimal basePrice) {
BigDecimal discount = basePrice.multiply(
BigDecimal.valueOf(discountPercentage / 100));
return basePrice.subtract(discount);
}
}
// Trigger refresh via actuator
// POST /actuator/refresh
// Or use Spring Cloud Bus for cluster-wide refresh
// POST /actuator/busrefreshService Discovery Questions
Service discovery enables dynamic microservices communication.
How do you configure Eureka for service discovery?
Eureka provides client-side service discovery where services register themselves and discover others through a central registry. The Eureka server maintains the registry and handles heartbeats from registered services. Client services use @LoadBalanced on their HTTP clients to automatically resolve service names to actual instances.
Eureka self-preservation reduces mass eviction when renewals unexpectedly fall below the configured threshold. Understand its availability/staleness trade-off and test failure behavior; do not copy production lease or self-preservation overrides merely to make a demo converge faster.
# Eureka Server
spring:
application:
name: eureka-server
eureka:
client:
register-with-eureka: false
fetch-registry: false
---
# Service Client
spring:
application:
name: order-service
eureka:
client:
service-url:
defaultZone: http://eureka:8761/eureka/
instance:
prefer-ip-address: trueHow do you create a load-balanced REST client with service discovery?
Load-balanced clients resolve service names to actual instance URLs using the service registry. The @LoadBalanced annotation on a RestClient.Builder bean enables this resolution. When you make requests using the service name as the host (like http://user-service), the load balancer intercepts the request, looks up available instances, and routes to one of them.
This abstraction means your code doesn't need to know about individual service instances or their locations—just the logical service name.
// Load-balanced RestClient
@Configuration
public class RestClientConfig {
@Bean
@LoadBalanced
public RestClient.Builder loadBalancedRestClientBuilder() {
return RestClient.builder();
}
}
@Service
public class UserClient {
private final RestClient restClient;
public UserClient(RestClient.Builder builder) {
this.restClient = builder
.baseUrl("http://user-service") // Service name, not URL
.build();
}
public User getUser(Long id) {
return restClient.get()
.uri("/api/users/{id}", id)
.retrieve()
.body(User.class);
}
}API Gateway Questions
API gateways provide a single entry point for microservices.
How do you configure Spring Cloud Gateway routes?
Spring Cloud Gateway routes requests to backend services based on predicates like path patterns, headers, or query parameters. Routes can include filters for request/response modification, rate limiting, circuit breaking, and retry logic. The lb:// prefix indicates load-balanced routing through service discovery.
Gateway configuration is declarative in YAML, making it easy to understand the routing topology at a glance. Filters can strip path prefixes, add headers, apply rate limits, or implement custom logic.
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
- name: CircuitBreaker
args:
name: orderServiceCB
fallbackUri: forward:/fallback/orders
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
methods: GET
backoff:
firstBackoff: 50ms
maxBackoff: 500ms
factor: 2
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@userKeyResolver}"How do you implement a custom global filter for authentication?
Global filters apply to all routes and are useful for cross-cutting concerns like authentication, logging, or request correlation. Implement GlobalFilter and Ordered interfaces, with lower order values executing first. The filter chain is reactive, using Mono<Void> for non-blocking execution.
You can validate credentials, reject unauthorized requests, and add verified context before forwarding. Token verification must be non-blocking on the WebFlux event loop or explicitly offloaded. Strip any client-supplied identity header before adding an internal one, and ensure downstream services cannot be reached through a path that bypasses the trusted gateway. Authentication at the edge does not remove the need for service-level authorization.
// Custom filters
@Component
public class AuthenticationFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders()
.getFirst(HttpHeaders.AUTHORIZATION);
if (token == null || !token.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// validateAndExtractUserId must not block the event loop.
String userId = validateAndExtractUserId(token);
ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
.headers(headers -> headers.remove("X-User-Id"))
.header("X-User-Id", userId)
.build();
return chain.filter(exchange.mutate().request(modifiedRequest).build());
}
@Override
public int getOrder() {
return -100; // Run early
}
}Distributed Tracing Questions
Distributed tracing tracks requests across service boundaries.
How do you configure distributed tracing with Spring Boot?
Spring Boot 4.1 Actuator auto-configures Micrometer Tracing with OpenTelemetry/OTLP or Brave/Zipkin bridges. Add the matching starter, configure an exporter, and build HTTP clients from the auto-configured RestClient.Builder, RestTemplateBuilder, or WebClient.Builder; constructing a client directly bypasses automatic propagation. Other transports and executor boundaries require supported instrumentation or explicit context propagation.
The default probability is 0.1. Choose head or tail sampling from traffic, incident-detection needs, backend limits, and cost; a small uniform sample can miss rare failures, so do not claim it is automatically representative.
# application.yml
spring:
application:
name: order-service
management:
tracing:
sampling:
probability: 1.0 # Sample all requests (reduce in production)
export:
zipkin:
endpoint: http://zipkin:9411/api/v2/spansHow do you create custom spans for detailed tracing?
While automatic instrumentation covers most cases, custom spans provide visibility into specific business operations. Create spans using the Tracer API, adding tags for searchable attributes and events for notable occurrences. Always use try-with-resources or finally blocks to ensure spans are properly closed, even when exceptions occur.
Avoid secrets, personal data, payment values, or unrestricted payloads in span attributes. High-cardinality attributes can increase storage and indexing cost; use only identifiers your telemetry policy and backend are designed to retain.
// Use instrumented clients and verify propagation across each transport
// and executor boundary used by your application.
// Manual span creation
@Service
@RequiredArgsConstructor
public class OrderService {
private final Tracer tracer;
public Order processOrder(Order order) {
Span span = tracer.nextSpan().name("process-order").start();
try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
span.tag("order.type", order.getType().name());
// Processing logic
validateOrder(order);
reserveInventory(order);
processPayment(order);
span.event("order-completed");
return order;
} catch (Exception e) {
span.error(e);
throw e;
} finally {
span.end();
}
}
}WebFlux Questions
WebFlux enables non-blocking, reactive applications.
What is the difference between Spring MVC and Spring WebFlux?
Spring MVC is the Servlet stack. Its common synchronous model holds a request thread while application code waits, but MVC also supports asynchronous return types and Spring Boot can use Java virtual threads. Spring WebFlux is the reactive stack; with Reactor Netty it usually runs application callbacks on a small event-loop group, so those callbacks must not perform blocking work.
WebFlux can reduce the number of waiting platform threads for high-concurrency, streaming, end-to-end non-blocking workloads. It does not turn JDBC, a blocking SDK, or CPU-heavy work into non-blocking work, and it adds reactive debugging and context-propagation costs. Compare both stacks under a representative workload instead of assuming one is faster.
flowchart LR
subgraph MVC["Spring MVC (Blocking)"]
direction LR
REQ1["Request"] --> THREAD["Thread<br/>(blocked, waits)"]
THREAD --> DB1["Database<br/>Query"]
DB1 --> THREAD
THREAD --> RES1["Response"]
end
subgraph WEBFLUX["Spring WebFlux (Non-Blocking)"]
direction LR
R1["Request 1"]
R2["Request 2"]
R3["Request N..."]
LOOP["Event Loop<br/>(few threads)"]
DB2["Database<br/>(async)"]
R1 --> LOOP
R2 --> LOOP
R3 --> LOOP
LOOP --> DB2
DB2 --> LOOP
endMVC: synchronous work is bounded by its request executor, but open connections and asynchronous requests are not identical to active request threads
WebFlux: a small event-loop group can multiplex many non-blocking operations, while downstream capacity and blocking work still impose limits
How do you write reactive controllers in WebFlux?
Reactive controllers return Mono<T> for single values and Flux<T> for multiple values. The reactive types are lazy—processing doesn't start until something subscribes. WebFlux handles subscription automatically when returning from controller methods. For streaming responses, use MediaType.TEXT_EVENT_STREAM_VALUE to send data as Server-Sent Events.
Error handling uses reactive operators like switchIfEmpty, onErrorResume, and timeout. These compose into a declarative pipeline that describes how to handle various scenarios without nested try-catch blocks.
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
// Return Mono for single value
@GetMapping("/{id}")
public Mono<Order> getOrder(@PathVariable String id) {
return orderService.findById(id);
}
// Return Flux for multiple values (streaming)
@GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Order> streamOrders() {
return orderService.findAll()
.delayElements(Duration.ofMillis(100)); // Simulate streaming
}
// Reactive request body
@PostMapping
public Mono<Order> createOrder(@RequestBody Mono<CreateOrderRequest> request) {
return request
.flatMap(orderService::create)
.doOnSuccess(order -> log.info("Created order: {}", order.getId()));
}
// Error handling
@GetMapping("/{id}/details")
public Mono<OrderDetails> getOrderDetails(@PathVariable String id) {
return orderService.findById(id)
.switchIfEmpty(Mono.error(new OrderNotFoundException(id)))
.flatMap(this::enrichWithDetails)
.timeout(Duration.ofSeconds(5))
.onErrorResume(TimeoutException.class,
e -> Mono.error(new ServiceUnavailableException("Timeout")));
}
}How do you use R2DBC for reactive database access?
R2DBC provides a reactive database SPI. It keeps database calls in the reactive pipeline, whereas JDBC calls block the calling thread and must not run on a WebFlux event loop. Spring Data repositories can extend ReactiveCrudRepository and return Mono and Flux types. Reactive transactions can be declarative with @Transactional and a ReactiveTransactionManager, or programmatic with TransactionalOperator as shown below.
The reactive transaction wraps the entire pipeline, ensuring atomicity across multiple database operations without blocking threads.
// Repository
public interface OrderRepository extends ReactiveCrudRepository<Order, String> {
Flux<Order> findByCustomerId(String customerId);
@Query("SELECT * FROM orders WHERE status = :status ORDER BY created_at DESC LIMIT :limit")
Flux<Order> findRecentByStatus(String status, int limit);
}
// Service with transactions
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final TransactionalOperator transactionalOperator;
public Mono<Order> createOrder(CreateOrderRequest request) {
return Mono.just(request)
.map(this::mapToOrder)
.flatMap(orderRepository::save)
.flatMap(this::reserveInventory)
.as(transactionalOperator::transactional); // Reactive transaction
}
}
// Configuration
@Configuration
@EnableR2dbcRepositories
public class R2dbcConfig extends AbstractR2dbcConfiguration {
@Override
@Bean
public ConnectionFactory connectionFactory() {
return ConnectionFactories.get(ConnectionFactoryOptions.builder()
.option(DRIVER, "postgresql")
.option(HOST, "localhost")
.option(PORT, 5432)
.option(DATABASE, "orders")
.option(USER, "user")
.option(PASSWORD, "password")
.build());
}
}How do you use WebClient for reactive HTTP calls?
WebClient is the reactive alternative to RestTemplate, providing non-blocking HTTP requests with a fluent API. Configure request interceptors for logging, authentication, or metrics. Response handling uses reactive operators—onStatus for error mapping, retryWhen for resilient retries with backoff.
For independent calls, Mono.zip subscribes to the sources and combines their results. Concurrency can reduce end-to-end latency, but it also multiplies downstream load; apply timeouts, bulkheads, and retry policy based on the operation's semantics. Never retry a non-idempotent request blindly.
@Service
public class ExternalApiClient {
private final WebClient webClient;
public ExternalApiClient(WebClient.Builder builder) {
this.webClient = builder
.baseUrl("https://api.external.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.filter(ExchangeFilterFunction.ofRequestProcessor(request -> {
log.debug("Request: {} {}", request.method(), request.url());
return Mono.just(request);
}))
.build();
}
public Mono<ExternalData> fetchData(String id) {
return webClient.get()
.uri("/data/{id}", id)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError,
response -> Mono.error(new ClientException("Client error")))
.onStatus(HttpStatusCode::is5xxServerError,
response -> Mono.error(new ServerException("Server error")))
.bodyToMono(ExternalData.class)
.retryWhen(Retry.backoff(3, Duration.ofMillis(100))
.filter(e -> e instanceof ServerException));
}
// Parallel calls
public Mono<AggregatedData> fetchAggregated(String userId) {
Mono<UserProfile> profileMono = fetchUserProfile(userId);
Mono<List<Order>> ordersMono = fetchUserOrders(userId).collectList();
Mono<Preferences> prefsMono = fetchPreferences(userId);
return Mono.zip(profileMono, ordersMono, prefsMono)
.map(tuple -> new AggregatedData(
tuple.getT1(),
tuple.getT2(),
tuple.getT3()
));
}
}Production Actuator Questions
Production applications require robust monitoring and health checks.
How do you configure Actuator endpoints for production?
Production Actuator configuration balances visibility with security. Expose only necessary endpoints, secure sensitive ones behind authentication, and configure health indicators for Kubernetes probes. The show-details setting controls whether health check details are visible—use when_authorized to require authentication for detailed health information.
Custom base paths like /management separate operational endpoints from application endpoints, and disk space thresholds alert you before storage issues cause failures.
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
base-path: /management
endpoint:
health:
show-details: when_authorized
probes:
enabled: true # Kubernetes probes
health:
diskspace:
threshold: 10GB
info:
git:
mode: simple
# Custom info
info:
app:
name: ${spring.application.name}
version: @project.version@
encoding: @project.build.sourceEncoding@How do you create custom health indicators?
Custom health indicators check dependencies that are not covered by auto-configured indicators. Implement HealthIndicator and return Health.up() or Health.down() with carefully sanitized details. Health indicators run when their health group is queried, so keep them bounded by a short timeout and cache where appropriate.
Do not put an external payment gateway in liveness: a gateway outage should not cause Kubernetes to restart every otherwise healthy application instance. Add a dependency to readiness only if the instance truly cannot serve useful traffic without it, and keep detailed exception output restricted.
// Custom health indicator
@Component
@RequiredArgsConstructor
public class PaymentGatewayHealthIndicator implements HealthIndicator {
private final PaymentGatewayClient client;
@Override
public Health health() {
try {
HealthCheckResponse response = client.healthCheckWithTimeout();
if (response.isHealthy()) {
return Health.up()
.withDetail("gateway", "Payment gateway is responsive")
.withDetail("latency", response.getLatencyMs() + "ms")
.build();
} else {
return Health.down()
.withDetail("gateway", "Payment gateway reports unhealthy")
.withDetail("reason", response.getReason())
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("gateway", "Cannot reach payment gateway")
.withException(e)
.build();
}
}
}How do you add custom business metrics?
Custom metrics track business KPIs beyond technical metrics. Use MeterRegistry to create counters for events, gauges for current values, and timers for durations. Tag metrics with dimensions like order type and region for detailed analysis.
Register gauges in @PostConstruct to track values that change over time, like active order count. Use counters for monotonically increasing values and timers for measuring operation durations with automatic histogram generation.
// Custom metrics
@Component
@RequiredArgsConstructor
public class OrderMetrics {
private final MeterRegistry registry;
private final AtomicLong activeOrders = new AtomicLong(0);
@PostConstruct
public void init() {
Gauge.builder("orders.active", activeOrders, AtomicLong::get)
.description("Number of orders being processed")
.register(registry);
}
public void recordOrderCreated(Order order) {
registry.counter("orders.created",
"type", order.getType().name(),
"region", order.getRegion()
).increment();
activeOrders.incrementAndGet();
}
public void recordOrderCompleted(Order order, long processingTimeMs) {
registry.timer("orders.processing.time",
"type", order.getType().name(),
"status", order.getStatus().name()
).record(Duration.ofMillis(processingTimeMs));
activeOrders.decrementAndGet();
}
}How do you implement graceful shutdown?
Graceful shutdown is enabled by default in current Spring Boot for embedded Tomcat, Jetty, and Reactor Netty. Configure its phase timeout to fit inside the orchestrator's termination grace period. For application-specific work, SmartLifecycle can control shutdown ordering—higher phases stop before lower phases.
The shutdown handler should stop accepting new work, wait for in-flight operations to complete, and then signal readiness for container termination. This prevents request failures during deployments.
spring:
lifecycle:
timeout-per-shutdown-phase: 30s@Component
@RequiredArgsConstructor
public class GracefulShutdownHandler implements SmartLifecycle {
private final OrderProcessor orderProcessor;
private boolean running = false;
@Override
public void start() {
running = true;
}
@Override
public void stop(Runnable callback) {
log.info("Initiating graceful shutdown...");
// Stop accepting new work
orderProcessor.stopAcceptingOrders();
// Wait for in-flight orders to complete
try {
orderProcessor.awaitCompletion(Duration.ofSeconds(25));
log.info("All orders processed, shutting down");
} catch (InterruptedException e) {
log.warn("Shutdown interrupted, some orders may be incomplete");
Thread.currentThread().interrupt();
}
running = false;
callback.run();
}
@Override
public boolean isRunning() {
return running;
}
@Override
public int getPhase() {
return Integer.MAX_VALUE; // Higher phases stop first
}
}Performance Tuning Questions
Production applications require careful tuning for performance and scalability.
How do you configure thread pools for high throughput?
Thread pool configuration significantly impacts application performance. Tomcat's thread pool handles incoming requests—size it based on expected concurrency and available CPU cores. Async task executors handle @Async methods and should be sized separately from the request handling pool.
Choose bounded queues and an explicit overload policy from the service contract. CallerRunsPolicy executes rejected work on the submitting thread; that can slow a producer, but in an HTTP path it can also consume the request thread and inflate tail latency. A rejected request, admission limit, or durable queue may be safer. When virtual threads are enabled with spring.threads.virtual.enabled=true, pool-sizing properties no longer control a dedicated virtual-thread pool.
server:
tomcat:
threads:
max: 200
min-spare: 20
accept-count: 100
connection-timeout: 10s
spring:
task:
execution:
pool:
core-size: 8
max-size: 50
queue-capacity: 100
keep-alive: 60s
thread-name-prefix: async-
scheduling:
pool:
size: 5
thread-name-prefix: scheduled-// Custom async executor
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("custom-async-");
executor.setRejectedExecutionHandler(new CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, params) -> {
log.error("Async method {} threw exception: {}",
method.getName(), throwable.getMessage(), throwable);
};
}
}
// Usage
@Service
public class NotificationService {
@Async
public CompletableFuture<Void> sendNotificationAsync(Notification notification) {
// This blocking work is offloaded to the Spring-managed @Async executor.
emailService.send(notification);
pushService.send(notification);
return CompletableFuture.completedFuture(null);
}
}How do you tune HikariCP connection pools?
Connection pool sizing affects queuing at both the application and database. Size the sum of pools across every replica against the database's usable connection budget and measured query service time. HikariCP leak detection is a diagnostic aid and can produce false positives for legitimately long operations. Set max-lifetime below any known infrastructure/database connection lifetime with jitter across instances; it is not a universal magic value.
Monitor pool metrics to understand actual usage patterns—idle connections consuming memory, threads waiting for connections, and connection acquisition times.
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30s
idle-timeout: 10m
max-lifetime: 29m
# Enable leak detection temporarily when diagnosing checkout leaks.
# leak-detection-threshold: 60s
pool-name: OrderServicePoolWith Actuator and Micrometer enabled, Spring Boot already instruments supported data sources. Observe active, idle, pending, timeout, query, and database-side metrics together instead of registering the same gauges on a schedule.
What JVM settings should you use for production Spring Boot applications?
There is no Spring-specific universal JVM flag set. Spring Boot 4.1.1 supports Java 17 through 26; begin with a supported JDK's container-aware ergonomics and an explicit memory budget. G1 is a general-purpose default on HotSpot, while ZGC is an option to evaluate when concurrent-collector CPU and memory trade-offs fit the latency objective. Equal -Xms/-Xmx, a collector switch, or a pause target should follow measurement rather than cargo-cult configuration.
Heap dumps and GC logs help diagnosis but may contain secrets and personal data, consume substantial disk, and need a protected writable destination plus retention policy. Avoid an arbitrary MaxMetaspaceSize: a low cap can create an artificial OutOfMemoryError. The following commands are starting shapes, not recommended values for every service.
# Production JVM settings
java -XX:+UseG1GC \
-Xmx2g \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/secure-diagnostics/heapdump.hprof \
-Xlog:gc*:file=/secure-diagnostics/gc.log:time,uptime:filecount=5,filesize=100m \
-jar app.jar
# Alternative to benchmark for a latency-sensitive workload
java -XX:+UseZGC \
-Xmx4g \
-jar app.jarQuick Reference
| Topic | Key Points |
|---|---|
| Auto-configuration | AutoConfiguration.imports, @Conditional, ordering |
| Custom starters | Two-module structure, ConfigurationProperties, metadata |
| Bean lifecycle | Instantiation → Population → Aware → Init → Destroy |
| Config precedence | Later property sources override earlier ones; tests and config-data ordering matter |
| Spring Cloud Config | Config server, @RefreshScope, encryption |
| Service Discovery | Eureka, @LoadBalanced, health checks |
| Gateway | Routes, filters, rate limiting, circuit breakers |
| WebFlux vs MVC | Non-blocking vs blocking, Mono/Flux, backpressure |
| Actuator | Health indicators, custom metrics, securing endpoints |
| Performance | Measure first; bound concurrency, queues, connection pools, memory, and downstream load |
Related Articles
- Spring Boot Interview Guide - Core Spring Boot fundamentals
- Microservices Architecture Interview Guide - Distributed systems patterns
- Java Core Interview Guide - Java language fundamentals
- Complete Java Backend Developer Interview Guide - Full Java backend preparation
Sources
- Spring Boot 4.1.1 system requirements - supported Java, Spring Framework, build tool, servlet container, and GraalVM versions
- Spring Boot externalized configuration - property source and config data precedence
- Spring Boot tracing - Micrometer Tracing, exporters, sampling, and client propagation
- Spring Boot graceful shutdown - default behavior and phase timeout
- Spring Cloud Gateway 5.0 configuration - current WebFlux route property namespace
- Spring Framework transaction management - declarative and programmatic reactive transaction options
Frequently Asked Questions
How does Spring Boot auto-configuration work internally?
Spring Boot auto-configuration is enabled through @EnableAutoConfiguration, included by @SpringBootApplication. Modern auto-configurations are listed in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. Conditions such as @ConditionalOnClass, @ConditionalOnMissingBean, and @ConditionalOnProperty decide whether definitions apply, while ordering annotations influence evaluation order. Defaults usually back off when the application supplies its own bean.
When should you create a custom Spring Boot starter?
Create a custom starter when several applications need the same opt-in dependency set and auto-configuration, such as an internal service integration or observability convention. Keep defaults overridable and avoid hiding application policy. Auto-configuration and the starter can be separate modules when consumers need dependency flexibility, but a two-module layout is not mandatory.
What is the difference between Spring MVC and Spring WebFlux?
Spring MVC is primarily the Servlet stack and commonly uses blocking request handling, although it also supports asynchronous responses and can use virtual threads. WebFlux is the reactive stack and is most useful when the end-to-end call path uses non-blocking APIs or streaming. WebFlux does not make blocking JDBC or SDK calls non-blocking, and neither stack is universally faster; choose after considering dependencies, concurrency, operational complexity, and measured workload behavior.
How do you implement distributed configuration with Spring Cloud Config?
A Config Server exposes configuration from a supported backend, and clients import it with spring.config.import=configserver:.... Use profiles and labels deliberately and decide whether startup should fail when the server is unavailable. @RefreshScope and Spring Cloud Bus can recreate selected beans after a refresh, but updates are not an atomic distributed transaction. Secure refresh endpoints and prefer a dedicated secret manager for secrets rather than treating a configuration repository as one.
How do you customize Spring Boot Actuator for production?
Expose only required Actuator endpoints, apply access controls and network policy, and avoid leaking sensitive health or info details. Keep liveness independent of downstream services; put carefully bounded dependency checks in readiness when losing that dependency makes the instance unable to serve. Add low-cardinality metrics and configure the default graceful shutdown timeout to match the platform termination budget.
What JVM and Spring settings should you tune for high-throughput applications?
There is no universal production flag set. Start with supported JDK and container-aware defaults, explicit memory limits, timeouts, and observability. Load-test the real workload, then tune the bottleneck: request concurrency, downstream limits, executor queues, database pool, allocation rate, heap, or collector. @Async only moves work to another executor; it does not make blocking I/O non-blocking or remove downstream capacity limits.
