Java Backend Interview Q&A← All stacks

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:

Ideal Answer:

java
@Service
public class OrderService {
    private final PaymentClient paymentClient;

    public OrderService(PaymentClient paymentClient) {
        this.paymentClient = paymentClient;
    }
}
java
@Service
class PricingService {
    Price calculate(Order order) {
        BigDecimal total = order.lines().stream()
                .map(OrderLine::subtotal)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        return new Price(total);
    }
}
java
@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);
    }
}
java
OrderService(@Qualifier("stripePaymentClient") PaymentClient paymentClient) {
    this.paymentClient = paymentClient;
}

Red Flags:


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:

Ideal Answer:

java
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);
}
java
@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);
    }
}

Red Flags:


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:

Ideal Answer:

yaml
# 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
java
@ConfigurationProperties("pricing")
@Validated
public record PricingProperties(
        @NotBlank String currency,
        @Min(1) int retryLimit) {}

Red Flags:


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:

Ideal Answer:

java
@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);
        }
    }
}
yaml
logging:
  level:
    root: INFO
    com.example.orders: INFO
    com.example.orders.pricing: DEBUG

Red Flags:


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:

Ideal Answer:

yaml
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}
java
@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();
    }
}

Red Flags:


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:

Ideal Answer:

java
@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));
    }
}
java
@Transactional(rollbackFor = IOException.class)
public void importFile(Path path) throws IOException {
    // database work
}

Red Flags:


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:

Ideal Answer:

java
@SpringBootApplication
public class OrdersApplication {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(OrdersApplication.class);
        application.setApplicationStartup(new BufferingApplicationStartup(2048));
        application.run(args);
    }
}
properties
management.endpoint.startup.enabled=true

Then read GET /actuator/startup to see which steps take the longest.

properties
spring.main.lazy-initialization=true

The 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.

Red Flags:


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:

Ideal Answer:

yaml
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

After SIGTERM the server stops accepting new connections and waits for active requests until the timeout expires.

java
@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.

yaml
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.

Red Flags:


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:

Ideal Answer:

properties
logging.structured.format.console=ecs

Older 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.

java
@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();
        }
    }
}
java
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.

Red Flags:


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:

Ideal Answer:

bash
./mvnw spring-boot:build-image \
  -Dspring-boot.build-image.imageName=registry.example.com/orders:1.4.0
dockerfile
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.

text
-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError

A fixed -Xmx that is larger than the limit is killed by the kernel before the JVM can react.

Red Flags:


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:

Ideal Answer:

text
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
java
@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());
    }
}
java
@ConfigurationProperties("platform.observability")
@Validated
public record PlatformProperties(
    boolean enabled,
    @NotBlank String serviceTag) {}
java
new ApplicationContextRunner()
    .withConfiguration(AutoConfigurations.of(
        ObservabilityAutoConfiguration.class))
    .withBean(ObservationHandler.class, CustomHandler::new)
    .run(context -> assertThat(context)
        .hasSingleBean(ObservationHandler.class));

Red Flags:


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:

Ideal Answer:

java
@Testcontainers
@SpringBootTest
class OrderFlowIT {
    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres =
            new PostgreSQLContainer<>("postgres:16");

    @Test
    void createsOrder() {
        // exercise the real HTTP and persistence flow
    }
}

Red Flags:


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:

Ideal Answer:

yaml
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: 3

The 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.

yaml
strategy:
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0

New pods become ready before old pods are removed, so capacity never drops below the desired count.

sql
ALTER TABLE orders ADD COLUMN customer_ref text;
CREATE INDEX CONCURRENTLY idx_orders_customer_ref ON orders (customer_ref);
  1. Add the new column and backfill it in small batches while the running code still uses the old column.
  2. Deploy code that writes both columns and reads the new one, and keep the old column for rollback.
  3. After the new version is stable everywhere, stop writing the old column and drop it in a later release.

Red Flags:


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:

Ideal Answer:

java
@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.

Red Flags:


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:

Ideal Answer:

java
@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();
    }
}

Red Flags:


Scoring Rubric

Level Pass bar
JuniorUses dependency injection, externalized configuration, logging, and HTTP responses correctly.
SeniorSets correct transaction boundaries, handles startup, shutdown, and downstream failures, and understands proxy behavior.
ArchitectBuilds observable platform conventions, safe releases, and a balanced, reliable test strategy.