Spring Boot — Interview Q&A Matrix
Parent stack: Spring Ecosystem Technology: Spring Boot Levels: Junior · Senior · Architect Questions: 5
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.
Senior
Q3 — 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.
Architect
Q4 — 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.
Q5 — 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.
Scoring Rubric
| Level | Pass bar |
|---|---|
| Junior | Uses dependency injection, scopes, validation, and HTTP responses correctly. |
| Senior | Defines correct transaction boundaries and understands proxy behavior. |
| Architect | Builds observable platform conventions and a balanced, reliable test strategy. |