Spring Boot — Interview Q&A Matrix
Parent stack: Spring Ecosystem Technology: Spring Boot Levels: Junior · Senior · Architect Questions: 15
Each entry contains a Question, the Expected Depth, an Ideal Answer, and Red Flags.
Junior
Q1 — Dependency injection and bean scopes
Question: What is dependency injection in Spring, why is constructor injection preferred, and what is the default bean scope?
Expected Depth:
- Explains inversion of control and how the application context creates and wires beans.
- Prefers constructor injection for required dependencies.
- Knows singleton, prototype, request, and session scopes.
- Understands that singleton beans must be safe for concurrent requests.
- Handles several beans of the same interface with
@Qualifieror@Primary. - Knows that injecting a prototype into a singleton does not create a new instance on every method call.
Ideal Answer:
- Spring's inversion-of-control container creates application objects, manages their lifecycle, and supplies their dependencies. Classes depend on interfaces instead of constructing infrastructure directly.
- Constructor injection is best for required dependencies. It supports
finalfields, complete initialization, and simple unit tests without a Spring context:
@Service
public class OrderService {
private final PaymentClient paymentClient;
public OrderService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
}- The default scope is singleton: one bean instance per application context. It is not the same as the GoF singleton pattern, and another context can create another instance.
- A singleton service handles many requests concurrently. Keep request-specific mutable state in method-local variables, not bean fields:
@Service
class PricingService {
Price calculate(Order order) {
BigDecimal total = order.lines().stream()
.map(OrderLine::subtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return new Price(total);
}
}- Other scopes include
prototype,request, andsession. If a singleton needs a new prototype instance for each operation, injectObjectProvider<T>and request an instance when needed:
@Service
class ReportService {
private final ObjectProvider<ReportContext> contexts;
ReportService(ObjectProvider<ReportContext> contexts) {
this.contexts = contexts;
}
Report create(ReportRequest request) {
ReportContext context = contexts.getObject();
return context.create(request);
}
}- When several beans implement one interface, select one explicitly:
OrderService(@Qualifier("stripePaymentClient") PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}- Circular constructor dependencies usually show that responsibilities should be separated. Do not hide them with field injection.
Red Flags:
- Uses field injection for every dependency and cannot explain the testing and initialization cost.
- Stores request data in a mutable singleton field.
- Thinks the default scope creates a bean for each HTTP request.
- Confuses a Spring singleton with one JVM-wide GoF singleton.
- Resolves circular dependencies only by switching to field injection.
- Assumes a prototype injected into a singleton is recreated for every call.
Q2 — @RestController request flow and validation
Question: Trace what happens when a JSON POST hits a @RestController method, and how do you validate the request body?
Expected Depth:
- Traces the request through filters,
DispatcherServlet, handler mapping, argument resolution, validation, and message conversion. - Uses Jakarta Bean Validation on request DTOs.
- Distinguishes malformed JSON, validation failure, business conflict, and unexpected failure.
- Returns suitable status codes and RFC 9457 Problem Details.
- Avoids exposing entities, stack traces, and sensitive internal messages.
- Knows when method validation with
@Validatedis required.
Ideal Answer:
- Servlet filters run first.
DispatcherServletthen finds the controller method through handler mappings, resolves its arguments, and uses anHttpMessageConvertersuch as Jackson to read JSON. - Use a request DTO with Jakarta Validation.
@Validtriggers validation after deserialization:
public record CreateOrder(
@NotBlank String sku,
@Min(1) int quantity) {}
@PostMapping("/orders")
public ResponseEntity<OrderResponse> create(
@Valid @RequestBody CreateOrder request) {
OrderResponse order = service.create(request);
URI location = URI.create("/orders/" + order.id());
return ResponseEntity.created(location).body(order);
}- Malformed JSON normally becomes
HttpMessageNotReadableException. Validation failure becomesMethodArgumentNotValidException. Map these and domain errors in one@RestControllerAdvice:
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
ResponseEntity<ProblemDetail> invalid(MethodArgumentNotValidException error) {
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
problem.setTitle("Request validation failed");
problem.setProperty("fieldErrors", error.getBindingResult()
.getFieldErrors()
.stream()
.map(field -> Map.of(
"field", field.getField(),
"message", field.getDefaultMessage()))
.toList());
return ResponseEntity.badRequest().body(problem);
}
}- Return
201 CreatedandLocationfor a new resource,400for invalid input,404for missing data, and409for a state conflict. - Use
@Validatedwhen validating method parameters outside request-body object validation, such as service method constraints. - Return API DTOs rather than JPA entities. Log the internal exception with a correlation ID, but do not send stack traces or database details to clients.
Red Flags:
- Parses normal JSON requests manually without a specific need.
- Uses JPA entities as request and response contracts.
- Validates only in the database and gives clients unclear errors.
- Returns
200 OKfor validation failures. - Returns stack traces, SQL messages, or secrets to the client.
- Creates different error formats in every controller.
Q3 — Externalized configuration and profiles
Question: You must run the same Spring Boot service in local, staging, and production with different database URLs and feature flags. How do you externalize configuration and select the right values for each environment?
Expected Depth:
- Uses
application.ymlwith profile-specific files such asapplication-prod.yml. - Explains the precedence order: command-line arguments and environment variables override profile files, which override the packaged defaults.
- Binds typed configuration groups with
@ConfigurationPropertiesinstead of scattered@Valueannotations. - Keeps secrets out of the repository and reads them from the platform or a secret manager.
- Activates profiles from the environment with
SPRING_PROFILES_ACTIVE, not from code. - Fails at startup when a required value is missing instead of using a wrong default.
Ideal Answer:
- Keep safe defaults in
application.ymland override them per profile:
# application.yml
spring:
application:
name: orders
jpa:
open-in-view: false
# application-prod.yml
spring:
datasource:
url: ${DB_URL}
username: ${DB_USER}
password: ${DB_PASSWORD}
pricing:
currency: EUR- Spring Boot collects property sources in a defined order. Command-line arguments and environment variables win over profile files, and profile files win over the values inside the artifact. Environment variable names use relaxed binding, so
SPRING_DATASOURCE_URLsetsspring.datasource.url. - Bind related keys to one validated object:
@ConfigurationProperties("pricing")
@Validated
public record PricingProperties(
@NotBlank String currency,
@Min(1) int retryLimit) {}- Register that class with
@EnableConfigurationPropertiesor@ConfigurationPropertiesScan, then injectPricingPropertieswhere needed. Typed binding gives validation, IDE completion, and one place to change a contract. - Leave a placeholder without a default when the value is mandatory. A missing
DB_URLthen fails the startup with a clear message instead of pointing silently at a local database. - Do not commit credentials. Inject them as environment variables from the platform or from a secret manager, and never print configuration values in logs.
Red Flags:
- Commits production credentials to the repository.
- Builds a different artifact per environment instead of promoting one artifact.
- Assumes the packaged
application.ymloverrides environment variables. - Scatters
@Value("${app.some.key}")across the code with no validation or documentation. - Selects profiles in code instead of from the deployment environment.
- Starts with a local database URL when the production variable is missing.
Q4 — Logging practice with SLF4J
Question: A developer adds System.out.println calls and an info log inside a loop that processes order lines. What logging practice should the service follow instead?
Expected Depth:
- Uses the SLF4J API with one logger per class instead of
System.outor a logging implementation directly. - Chooses levels by purpose:
errorfor failed operations,warnfor handled anomalies,infofor lifecycle events,debugfor diagnosis. - Uses parameterized messages instead of string concatenation.
- Configures per-package levels instead of enabling debug everywhere.
- Keeps secrets, tokens, and personal data out of log lines.
- Logs one event per meaningful state change, not one line per loop iteration.
Ideal Answer:
- Declare a logger per class and pass the exception as the last argument so the stack trace is printed once:
@Service
class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
void place(Order order) {
log.debug("Placing order {}", order.id());
try {
// payment and persistence work
} catch (PaymentException error) {
log.error("Payment failed for order {}", order.id(), error);
}
}
}- Parameterized logging formats the message only when the level is enabled, and it avoids building strings on a hot path.
- Set levels in configuration so one package can be raised without touching code:
logging:
level:
root: INFO
com.example.orders: INFO
com.example.orders.pricing: DEBUG- Log the failure where it is handled, once. A log line in every layer repeats the same event and makes the cause harder to find.
- Never log secrets, tokens, passwords, or full personal payloads. Log an identifier, and let a trace or a request log carry the rest.
- A request identifier in the logging context lets one request be followed across lines. The mechanics belong to the observability questions at the senior level.
Red Flags:
- Uses
System.out.printlnorThrowable.printStackTrace()in application code. - Concatenates strings in log calls on a hot path.
- Logs inside a loop for every record and floods storage.
- Logs passwords, tokens, or full request bodies.
- Catches an exception, logs nothing, and returns a default value.
- Enables
DEBUGforrootin production.
Q5 — Actuator health and info endpoints
Question: The operations team wants to check whether a running service is healthy and which version it runs, without opening a shell on the host. Which Actuator endpoints do you expose, and how do you keep them safe?
Expected Depth:
- Adds
spring-boot-starter-actuatorand knows the common endpoints:health,info,metrics,loggers,env. - Knows that only
healthis exposed over HTTP by default. - Uses an explicit exposure list such as
health,info,metricsinstead of exposing everything. - Uses health groups for liveness and readiness and understands how Kubernetes consumes them.
- Adds a custom indicator for a dependency that matters to the service.
- Protects or removes endpoints that reveal configuration or change behavior.
Ideal Answer:
- Expose a short allow list and keep details hidden from anonymous callers:
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: when-authorized
probes:
enabled: true
info:
app:
name: orders
version: ${APP_VERSION:unknown}healthis the only endpoint exposed by default.env,configprops,heapdump, andloggersreveal internals or change behavior, so keep them off the public port or protect them with the security configuration.- Liveness answers whether the process works. Readiness answers whether the instance should receive traffic. A failed liveness probe restarts the pod, and a failed readiness probe removes it from the load balancer.
- Add an indicator only for a dependency that changes the ability to serve traffic:
@Component
class PricingHealthIndicator implements HealthIndicator {
private final PricingClient client;
PricingHealthIndicator(PricingClient client) {
this.client = client;
}
@Override
public Health health() {
boolean reachable = client.ping();
return reachable
? Health.up().withDetail("pricing", "reachable").build()
: Health.down().withDetail("pricing", "unreachable").build();
}
}- Do not let an optional dependency fail readiness. If every instance leaves the load balancer because a reporting service is slow, the outage becomes larger than the dependency problem itself.
Red Flags:
- Exposes
*and publishesenv,heapdump, orthreaddumpon the public port. - Uses one health check for both liveness and readiness.
- Wires an optional dependency into readiness and removes healthy instances from traffic.
- Reports
upwithout checking anything meaningful. - Leaves management endpoints unauthenticated while they are reachable from the internet.
Senior
Q6 — Transaction propagation and self-invocation
Question: A method annotated @Transactional(propagation = REQUIRES_NEW) does not open a new transaction when another method in the same class calls it. Explain why and how propagation works.
Expected Depth:
- Explains proxy-based transaction interception and self-invocation.
- Compares
REQUIRED,REQUIRES_NEW, andNESTEDaccurately. - Knows that
NESTEDrequires savepoint support and depends on the transaction manager. - Explains default rollback rules for runtime exceptions, errors, and checked exceptions.
- Understands connection-pool and consistency costs of
REQUIRES_NEW. - Keeps transactions short and avoids remote calls inside them.
Ideal Answer:
- In the normal proxy mode, Spring starts a transaction only when a call enters through the bean proxy. A call such as
this.recordAudit()stays inside the object and bypasses the proxy, so the annotation on that method is not applied. - Move the independent operation to another bean:
@Service
class InvoiceService {
private final AuditService auditService;
@Transactional
public void process(Invoice invoice) {
saveInvoice(invoice);
auditService.record(invoice.id());
}
}
@Service
class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void record(Long invoiceId) {
auditRepository.save(new AuditEntry(invoiceId));
}
}REQUIREDjoins an existing transaction or starts one.REQUIRES_NEWsuspends the outer transaction and starts an independent transaction. It normally needs another database connection while the outer connection remains held.NESTEDuses a savepoint inside one physical transaction. It works only when the transaction manager and resource support savepoints; it is not the same asREQUIRES_NEW.- By default, Spring rolls back on
RuntimeExceptionandError, but not on checked exceptions. UserollbackForonly when the business rule requires it:
@Transactional(rollbackFor = IOException.class)
public void importFile(Path path) throws IOException {
// database work
}- An inner
REQUIREDmethod that marks the transaction rollback-only can causeUnexpectedRollbackExceptionwhen the outer method tries to commit. - Keep transactions around database work. Do not hold locks and connections while calling a slow remote service.
Red Flags:
- Expects
this.method()to apply a different transaction annotation. - Says that every checked exception rolls back by default.
- Treats
NESTEDandREQUIRES_NEWas identical. - Uses
REQUIRES_NEWheavily without checking connection-pool size and failure semantics. - Catches an exception inside a transaction and ignores a rollback-only state.
- Holds a transaction open during HTTP calls or long user interaction.
Q7 — Diagnosing a slow application startup
Question: A Spring Boot service takes 45 seconds to start, so pod rollouts time out before the new replicas become ready. How do you find the slow phase, and when is lazy initialization the right fix?
Expected Depth:
- Measures startup with the
startupActuator endpoint and aBufferingApplicationStartupbean. - Separates bean creation, class loading, JPA initialization, connection-pool work, and remote calls made during startup.
- Looks for network calls in
@PostConstructmethods,CommandLineRunnerbeans, and@EventListenermethods. - Knows the trade-offs of
spring.main.lazy-initialization: a faster start, a slower first request, and errors moving to runtime. - Tunes the Kubernetes
startupProbeas a complement, not as the fix. - Repeats the measurement after every change.
Ideal Answer:
- Record the startup steps first, then optimize with evidence:
@SpringBootApplication
public class OrdersApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(OrdersApplication.class);
application.setApplicationStartup(new BufferingApplicationStartup(2048));
application.run(args);
}
}management.endpoint.startup.enabled=trueThen read GET /actuator/startup to see which steps take the longest.
- Common causes are a remote call in a
@PostConstructmethod, eager loading of reference data, JPA schema validation against a slow database, a large component scan, and warmup loops in a runner. - Change one cause at a time. Do not call remote services while the context starts; load that data on first use or after the application is ready. Keep only the beans that must exist before traffic arrives, and check whether schema validation and pool warmup cost more than they protect.
- Treat lazy initialization as a trade, not as a default:
spring.main.lazy-initialization=trueThe context starts faster, but a configuration error appears on the first request instead of at startup, and that first request is slower. Beans that must react to events or run background work still need eager creation.
- For genuinely large applications, align the platform with the reality. Give the
startupProbea generousfailureThresholdandperiodSecondsso a slow start is not read as a crash, then continue to reduce the real cost. - A build-time AOT step or a class data sharing archive can cut startup further. Both add build complexity, so measure the gain before adopting them.
Red Flags:
- Turns on lazy initialization everywhere without measuring the start.
- Does not see the remote call inside a
@PostConstructmethod. - Increases probe thresholds and calls the problem solved.
- Disables schema validation or migrations only to make the numbers look better.
- Blames the JVM or the container without startup data.
- Tests startup with a warm database and a fast network only.
Q8 — Graceful shutdown and connection draining
Question: During a rolling deployment, clients see connection resets and 502 responses. How do you make shutdown graceful, and how do you confirm that in-flight work finishes?
Expected Depth:
- Enables
server.shutdown=gracefuland sets a drain bound withspring.lifecycle.timeout-per-shutdown-phase. - Knows which work the web server drains and which it does not: message listeners, schedulers, and thread pools need explicit stopping.
- Aligns
terminationGracePeriodSecondswith the drain timeout plus the endpoint propagation delay. - Understands that Kubernetes removes the pod from Service endpoints at the same time as it sends SIGTERM, so a short
preStopdelay covers the propagation gap. - Stops consumers with
SmartLifecycleor a shutdown listener so work is not cut in the middle. - Measures the drain with logs, metrics, and client-side error rates.
Ideal Answer:
- Turn on graceful shutdown and give it a bound:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30sAfter SIGTERM the server stops accepting new connections and waits for active requests until the timeout expires.
- The web-server drain does not cover background work. Stop consumers and schedulers explicitly, in a phase that runs before shared pools close:
@Component
class ConsumerLifecycle implements SmartLifecycle {
private final MessageConsumer consumer;
private volatile boolean running;
ConsumerLifecycle(MessageConsumer consumer) {
this.consumer = consumer;
}
@Override
public void start() {
consumer.subscribe();
running = true;
}
@Override
public void stop() {
consumer.drainAndClose();
running = false;
}
@Override
public boolean isRunning() {
return running;
}
@Override
public int getPhase() {
return Integer.MAX_VALUE;
}
}A higher phase starts later and stops earlier, which keeps the consumer alive while the web layer drains.
- Give the platform a grace period that is longer than the
preStopdelay plus the drain timeout:
spec:
template:
spec:
terminationGracePeriodSeconds: 45
containers:
- name: orders
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"]Endpoint removal takes time to reach every load balancer. The short sleep keeps the pod serving during that window, and the grace period must cover both delays. If it does not, the kubelet kills the process while requests are still active.
- Do not rely on a shutdown hook as the only protection. Write critical state when it is produced, and make clients retry safe operations.
- Confirm the behavior. Count the requests that were still active when the phase ended, watch for the drain timeout warning in the log, and compare client error rates for each rollout.
Red Flags:
- Sets a
terminationGracePeriodSecondsthat is shorter than the drain timeout. - Assumes
server.shutdown=gracefulalso stops message listeners and scheduled jobs. - Copies a
preStopsleep without checking how long endpoint removal takes in that cluster. - Flushes buffered data only at the last moment, in code the platform can skip.
- Has no way to tell whether a request was in flight when the process stopped.
- Deploys a single replica and expects zero client errors during a release.
Q9 — Structured logging and correlation across services
Question: Logs from forty pods are hard to search, and one request crosses three services. How do you make the logs structured and traceable without flooding the log store?
Expected Depth:
- Emits one structured event per line, with stable field names the aggregator can index.
- Puts a request or trace identifier in every line and returns it to the caller.
- Knows that the logging context is thread-local and is lost across threads and asynchronous boundaries.
- Propagates that context into thread pools and asynchronous code explicitly.
- Joins logs to traces with Micrometer Tracing and a bridge such as OpenTelemetry.
- Controls volume and keeps personal data and secrets out of the fields.
Ideal Answer:
- Prefer structured output over free text. On Spring Boot 3.4 and later the console encoder can emit a structured format directly:
logging.structured.format.console=ecsOlder versions need an encoder library in the logging configuration, but the rule is the same: one event per line, with fields such as timestamp, level, service, correlationId, and message.
- Add the correlation identifier at the HTTP boundary and return it to the caller:
@Component
class CorrelationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String correlationId = request.getHeader("X-Correlation-Id");
if (correlationId == null) {
correlationId = UUID.randomUUID().toString();
}
MDC.put("correlationId", correlationId);
response.setHeader("X-Correlation-Id", correlationId);
try {
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}- The logging context is thread-local. Work that moves to another thread loses it, so copy the context into the executor:
class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable task) {
Map<String, String> context = MDC.getCopyOfContextMap();
return () -> {
if (context != null) {
MDC.setContextMap(context);
}
try {
task.run();
} finally {
MDC.clear();
}
};
}
}Register it with ThreadPoolTaskExecutor#setTaskDecorator. Without this step, asynchronous lines carry no identifier and the trace breaks.
- For a request path across services, add Micrometer Tracing with an OpenTelemetry bridge. The trace and span identifiers appear in the logging context, so a log search and a trace view describe the same event.
- Control volume: no line per record,
debugonly for the package under investigation, and sampling or filtering at the collector. Bounded field values keep the indexes small. - Keep secrets, tokens, and personal data out of log fields. Log identifiers, and let an authorized lookup return the details.
Red Flags:
- Writes free-text messages with values appended and expects the aggregator to parse them.
- Emits one log line per processed record.
- Reads the logging context in another thread and assumes the value is present.
- Logs full request or response bodies that contain personal data.
- Enables
debugfor the whole application in production to compensate for weak messages. - Has no identifier that joins the logs from three services into one request.
Q10 — Packaging and container images
Question: The service ships as a 400 MB image, and every build pushes one huge layer. How do you package a Spring Boot service for Kubernetes so builds, pulls, and rollouts stay fast?
Expected Depth:
- Compares the executable jar, buildpack images, and a layered Docker build.
- Explains layer ordering by change frequency, so an application code change invalidates only the smallest layer.
- Uses a pinned JRE base image, a non-root user, and no build tooling in the runtime image.
- Sets container-aware JVM options instead of a fixed
-Xmxthat ignores the pod limit. - Keeps configuration and secrets out of the image.
- Knows the trade-offs of a native image: fast startup, longer build, and reflection hints.
Ideal Answer:
- Start from the build tool. Buildpacks produce a layered image with a JRE and a non-root user, and they do not copy build output by hand:
./mvnw spring-boot:build-image \
-Dspring-boot.build-image.imageName=registry.example.com/orders:1.4.0- If the team keeps a
Dockerfile, unpack the layered jar so dependencies and application classes live in separate layers:
FROM eclipse-temurin:21-jre AS builder
WORKDIR /app
COPY target/orders.jar application.jar
RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/extracted/dependencies/ ./
COPY --from=builder /app/extracted/spring-boot-loader/ ./
COPY --from=builder /app/extracted/snapshot-dependencies/ ./
COPY --from=builder /app/extracted/application/ ./
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "org.springframework.boot.loader.launch.JarLauncher"]The layer order matters: dependency layers change rarely, and the application layer changes on every build. The jar mode and the launcher class name differ between Spring Boot versions, so confirm both against the documentation for the version in use.
- Express the heap as a share of the container limit, and let the JVM read the limit:
-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryErrorA fixed -Xmx that is larger than the limit is killed by the kernel before the JVM can react.
- Run as a non-root user, keep secrets out of the image, and inject configuration at runtime. One artifact then moves between environments unchanged.
- Native image trade-off: startup falls to a fraction of a second and memory use drops, while the build takes longer and reflective or dynamic code needs runtime hints. Adopt it for latency-sensitive scaling, not as a default.
Red Flags:
- Uses a full JDK image with build tools for the production runtime.
- Copies the whole target directory and installs dependencies inside the runtime image.
- Bakes environment-specific URLs or credentials into the image.
- Runs the process as root.
- Sets a fixed
-Xmxthat exceeds the container memory limit. - Ships one huge layer, so every code change also invalidates the dependency layer.
- Builds a native image without testing reflection, resources, and dynamic proxies.
Architect
Q11 — Auto-configuration and building a custom starter
Question: As architect you want to standardize cross-cutting concerns (tracing, metrics, resilience defaults) across 30 services. How does Spring Boot auto-configuration work, and how would you build a shared starter?
Expected Depth:
- Explains starter dependencies versus auto-configuration code.
- Registers Boot 3 auto-configuration through
AutoConfiguration.imports. - Uses conditions that back off when an application provides its own bean.
- Exposes validated, typed
@ConfigurationPropertiesand clear opt-out controls. - Tests auto-configuration with
ApplicationContextRunner. - Manages versions with a BOM and considers AOT/native-image requirements.
Ideal Answer:
- Separate the library into an auto-configuration module and a small starter module. The starter mainly declares dependencies; the auto-configuration module contains beans and properties.
- In Spring Boot 3, list auto-configuration classes in:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports- Use conditions so configuration applies only when relevant and backs off when the application supplies its own bean:
@AutoConfiguration
@ConditionalOnClass(MeterRegistry.class)
@ConditionalOnProperty(
prefix = "platform.observability",
name = "enabled",
havingValue = "true",
matchIfMissing = true)
@EnableConfigurationProperties(PlatformProperties.class)
public class ObservabilityAutoConfiguration {
@Bean
@ConditionalOnMissingBean
ObservationHandler<?> platformObservationHandler(
PlatformProperties properties) {
return new PlatformObservationHandler(properties.serviceTag());
}
}- Expose validated, typed settings with metadata for IDE completion:
@ConfigurationProperties("platform.observability")
@Validated
public record PlatformProperties(
boolean enabled,
@NotBlank String serviceTag) {}- Test default behavior, opt-out, missing classes, and user overrides with
ApplicationContextRunner:
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
ObservabilityAutoConfiguration.class))
.withBean(ObservationHandler.class, CustomHandler::new)
.run(context -> assertThat(context)
.hasSingleBean(ObservationHandler.class));- Publish a BOM to align starter and dependency versions. Use semantic versioning and a deprecation period for breaking configuration changes.
- Document every default and opt-out. Add runtime hints when reflection or resources must work in native images.
Red Flags:
- Copies the same configuration into every service.
- Creates beans unconditionally and prevents application overrides.
- Hides important network calls or security behavior inside auto-configuration.
- Provides no typed properties, validation, documentation, or opt-out.
- Tests only inside one full application and never tests condition combinations.
- Publishes incompatible dependency versions without a BOM or migration guide.
Q12 — Testing strategy across the Spring stack
Question: Define a pragmatic testing strategy for a Spring Boot service so teams get confidence without slow, flaky suites.
Expected Depth:
- Separates unit, slice, integration, contract, and end-to-end tests.
- Chooses
@WebMvcTest,@DataJpaTest, or@SpringBootTestfor a clear reason. - Uses Testcontainers for production database and broker behavior.
- Tests external HTTP contracts with a stub server and provider/consumer contracts where useful.
- Reuses Spring contexts and avoids unnecessary
@DirtiesContext. - Keeps tests deterministic, isolated, observable, and suitable for CI.
Ideal Answer:
- Use plain unit tests for domain rules. They should not start Spring.
- Use
@WebMvcTestfor controller routing, validation, security rules, and response JSON. Use@DataJpaTestfor mappings, queries, constraints, and transaction behavior. - Use a smaller number of
@SpringBootTesttests for complete application flows. Run PostgreSQL, Kafka, or Redis with Testcontainers when database or broker behavior matters. - Spring Boot 3.1+ can connect a container with
@ServiceConnection:
@Testcontainers
@SpringBootTest
class OrderFlowIT {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16");
@Test
void createsOrder() {
// exercise the real HTTP and persistence flow
}
}- Stub external HTTP services with WireMock or MockWebServer and test timeout and error mapping. Use contract tests when independently deployed services share an API.
- Keep most tests below the full-context level. Reuse a small number of context configurations so Spring's context cache can help. Avoid
@DirtiesContextunless a test truly changes global context state. - Reset database and broker state between tests. Do not depend on test order, fixed sleeps, or shared mutable fixtures.
- Reserve end-to-end tests for a few critical user flows. They are valuable but slower and harder to diagnose.
Red Flags:
- Uses
@SpringBootTestfor every class. - Uses H2 to approve PostgreSQL-specific SQL and mappings.
- Mocks the repository in every test and never tests real mappings or constraints.
- Uses fixed sleeps for asynchronous tests.
- Shares dirty database or broker state between tests.
- Uses
@DirtiesContextroutinely and makes the suite slow. - Has no contract or failure-path tests for external integrations.
Q13 — Zero-downtime releases and schema evolution
Question: Design the release process for a Spring Boot service that must stay available during every deployment, including a release that removes a column that production still uses.
Expected Depth:
- Separates startup, readiness, and liveness probes and gives each a single purpose.
- Pairs graceful shutdown with traffic draining and safe client retries.
- Uses expand-and-contract migrations so old and new versions run against one schema.
- Rolls out with surge capacity and rolls back on a signal such as the error rate.
- Handles in-flight messages and duplicate delivery during the overlap window.
- States the compatibility rule: each release must work with the previous schema and with the next one.
Ideal Answer:
- Three probes, three purposes:
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 2
failureThreshold: 30
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
failureThreshold: 3The startup probe protects a slow start from being read as a crash. Readiness controls traffic. Liveness restarts a process that cannot recover. Liveness must not depend on databases or brokers, or one dependency outage becomes a restart loop.
- Roll out with spare capacity:
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0New pods become ready before old pods are removed, so capacity never drops below the desired count.
- Remove a column over three releases, not one:
ALTER TABLE orders ADD COLUMN customer_ref text;
CREATE INDEX CONCURRENTLY idx_orders_customer_ref ON orders (customer_ref);- Add the new column and backfill it in small batches while the running code still uses the old column.
- Deploy code that writes both columns and reads the new one, and keep the old column for rollback.
- After the new version is stable everywhere, stop writing the old column and drop it in a later release.
- Rollback must not require a schema rollback. Keep the previous image tag, and keep the schema compatible with the previous release for at least one release cycle.
- The same rule applies to event schemas. Consumers must tolerate both payload shapes during the overlap, and they must deduplicate because a rebalance or a retry can deliver a message twice.
- Watch error rate, latency, and consumer lag per version. Automatic rollback on a breached threshold is faster than a manual decision during an incident.
- Practice the path. The mixed-version window is the risky state, so it must appear in load tests and rollback drills.
Red Flags:
- Points the liveness probe at a health endpoint that checks the database.
- Drops or renames a column in the same release as the code change.
- Assumes a rollback can reverse a completed migration.
- Runs one replica with
maxUnavailableabove zero and calls the release zero-downtime. - Ignores messages and events that are in flight during the overlap window.
- Tests the new version alone and never the mixed-version state.
Q14 — Configuration and secret platform
Question: Thirty services need one configuration and secret strategy across three environments. What do you centralize, what stays local, and how do the services keep running when the configuration source is unavailable?
Expected Depth:
- Separates defaults in the artifact, environment configuration, and secrets, and gives each a different home.
- Promotes one artifact across environments and changes only the configuration source.
- Chooses between Kubernetes ConfigMaps and Secrets, Git-backed configuration, and a secret manager.
- Distinguishes values that can be refreshed at runtime from values that need a rollout.
- Defines behaviour for a configuration outage: running services keep serving, new instances retry and fail clearly.
- Audits who changed what, and keeps secrets out of images, logs, and the repository.
Ideal Answer:
- Split configuration into three tiers with different lifetimes:
- Artifact defaults: safe, non-sensitive values that rarely change.
- Environment configuration: URLs, timeouts, and feature flags, stored as ConfigMaps or Git-backed configuration.
- Secrets: credentials, tokens, and keys, stored in a secret manager and injected at runtime.
- Build the image once and promote it. Only the configuration source changes between environments. A different artifact per environment means the tested artifact is not the released one.
- Prefer workload identity and short-lived credentials over long-lived secrets copied into a ConfigMap. The platform then rotates credentials, and no human handles them.
- Separate what can change at runtime from what cannot:
@RefreshScope
@Component
class PricingGateway {
@Value("${pricing.surcharge-percent}")
private int surchargePercent;
}A refresh recreates the bean and reads the new value. Values that own a connection, a port, or a thread pool usually need a rollout, because the resource is built once at startup.
- Plan for the outage. The configuration source sits on the startup path, so state the rule plainly: running instances keep the last known good values and must not fail, and a new instance retries with a bounded backoff and fails loudly rather than starting with hidden defaults.
- Keep a copy of the last good configuration in the runtime environment, so a short configuration outage does not block a scaling event during a traffic spike.
- Audit and change control: configuration lives in version control with review, secret access is audited in the secret manager, and every running pod can report which configuration version it uses.
Red Flags:
- Copies the same secret into every service namespace or into Git.
- Branches application code per environment instead of externalizing values.
- Assumes a refresh rebuilds connection pools or re-binds ports automatically.
- Crashes healthy running services because the configuration server had a brief outage.
- Lets developers read production secrets with no audit record.
- Cannot tell which configuration version a running pod uses.
Q15 — Observability standard for a service fleet
Question: Define the observability standard for a fleet of Spring Boot services. What must every service emit, how do you keep the cost predictable, and what does on-call need during an incident?
Expected Depth:
- Standardizes metrics, logs, and traces and states which question each signal answers.
- Uses Micrometer for metrics and Micrometer Tracing with a bridge for distributed traces.
- Allows a small, bounded label set and bans unbounded labels such as user or order identifiers.
- Defines service level objectives with error budgets and alerts on user-visible symptoms.
- Controls cost with trace sampling, log volume budgets, and retention tiers.
- Ensures one key joins dashboards, logs, and traces during an incident.
Ideal Answer:
- Require three signals from every service:
- Metrics: request rate, latency, and error ratio per endpoint, JVM memory and threads, connection-pool usage, and consumer lag where relevant. Actuator and Micrometer provide most of them without application code.
- Logs: one structured event per meaningful state change, with the service name and trace context.
- Traces: spans for inbound requests and outbound calls, with context propagated through HTTP headers and message headers.
- Keep labels bounded. The route template is safe, while the raw URL, the user identifier, and the order identifier are not, because each new value creates a new time series:
@Service
class OrderMetrics {
private final Counter rejected;
OrderMetrics(MeterRegistry registry) {
this.rejected = Counter.builder("orders.rejected")
.description("Orders rejected by validation")
.tag("reason", "validation")
.register(registry);
}
void recordRejection() {
rejected.increment();
}
}- Sample traces deliberately: a low rate in normal operation, keep errors, and allow a forced sample for one identifier while investigating. Sampling that hides failures defeats its own purpose.
- Set service level objectives on the journeys users care about, and alert on the burn rate of the error budget. An alert on every cause pages the team for events nobody can act on.
- Budget the data: a log volume target per service, retention tiers for hot and cold storage, and dashboards that on-call actually opens.
- Make one identifier join the three signals. A trace identifier in logs, in the response header, and in the incident ticket turns a three-service investigation into one search.
- Deliver the standard as platform configuration, not as a document. A shared auto-configuration makes the compliant path the default path, and any exception goes through an architecture decision record.
Red Flags:
- Uses user identifiers, order identifiers, or raw URLs as metric labels.
- Alerts on every cause and pages for conditions nobody can act on.
- Samples traces in a way that hides failures.
- Keeps
debuglogging on to avoid building useful dashboards. - Has no retention or ownership policy for logs, metrics, and traces.
- Documents the standard but leaves every service to implement it alone.
Scoring Rubric
| Level | Pass bar |
|---|---|
| Junior | Uses dependency injection, externalized configuration, logging, and HTTP responses correctly. |
| Senior | Sets correct transaction boundaries, handles startup, shutdown, and downstream failures, and understands proxy behavior. |
| Architect | Builds observable platform conventions, safe releases, and a balanced, reliable test strategy. |