Spring Boot — Interview Questions

Stack context: This system uses Spring Boot 3.3.0 with Java 21. All six services are Spring Boot applications using Spring MVC (or WebFlux for the gateway), Spring Data JPA, Spring Data Redis, Spring Kafka, Spring Security, Springdoc OpenAPI, Micrometer/Zipkin tracing, and Spring Boot Actuator.


Q1 — What is Spring Boot and how does it differ from plain Spring Framework? junior

Answer: Spring Boot is an opinionated, convention-over-configuration layer on top of Spring Framework that eliminates most boilerplate configuration.

Key differences:

Feature Spring Framework Spring Boot
Setup Manual bean definitions, XML or Java config Auto-configuration via @SpringBootApplication
Embedded server No — deploy WAR to external Tomcat Yes — embedded Tomcat/Netty via spring-boot-starter-web
Dependency management Manual version coordination Spring Boot BOM manages compatible versions
Actuator Not included Built-in /actuator endpoints
Properties Scattered XML configs Centralized application.yml

Auto-configuration: Spring Boot scans classpath for known dependencies and configures beans automatically. If spring-data-redis is on classpath, it configures RedisTemplate with defaults from application.yml.


Q2 — What is @SpringBootApplication and what annotations does it compose? junior

Answer: @SpringBootApplication is a composed annotation combining:

  1. @SpringBootConfiguration: Marks the class as a configuration source (specialization of @Configuration).
  2. @EnableAutoConfiguration: Triggers Spring Boot's auto-configuration mechanism — reads META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.
  3. @ComponentScan: Scans the package (and sub-packages) of the annotated class for @Component, @Service, @Repository, @Controller beans.
@SpringBootApplication   // = all three above
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

Tip: @ComponentScan scans the package of the main class. Always place the main class in the root package (e.g., com.ecommerce.order) so all sub-packages are scanned.


Q3 — What is dependency injection and what are the three ways to inject beans in Spring? junior

Answer: Dependency Injection (DI) is a design pattern where an object's dependencies are provided externally rather than created internally — enabling loose coupling and testability.

Three injection styles:

1. Constructor injection (recommended):

@Service
public class OrderService {
    private final OrderRepository repo;
    private final KafkaTemplate<String, OrderCreatedEvent> kafka;

    public OrderService(OrderRepository repo, KafkaTemplate<String, OrderCreatedEvent> kafka) {
        this.repo = repo;
        this.kafka = kafka;
    }
}

2. Setter injection:

@Autowired
public void setRepo(OrderRepository repo) { this.repo = repo; }

3. Field injection (discouraged in production):

@Autowired
private OrderRepository repo;

Why constructor injection is preferred:


Q4 — What is the difference between @Component, @Service, @Repository, and @Controller? junior

Answer: All four are specializations of @Component — they all mark a class for auto-detection and Spring-managed bean creation. The differences are semantic and functional:

Annotation Layer Extra behavior
@Component Generic None — pure marker
@Service Business logic None — semantic clarity
@Repository Data access Enables Spring's persistence exception translation (PersistenceExceptionTranslationPostProcessor)
@Controller Web layer (MVC) Enables request mapping and view resolution
@RestController Web layer (REST) @Controller + @ResponseBody — returns JSON/XML directly

Practical effect: @Repository wraps JDBC/JPA exceptions into Spring's unified DataAccessException hierarchy. @Controller integrates with DispatcherServlet.


Q5 — What is Spring's application context and how does bean lifecycle work? junior

Answer: The ApplicationContext is Spring's IoC container — it manages bean creation, configuration, and lifecycle.

Bean lifecycle:

  1. Instantiation: Spring calls constructor (or factory method).
  2. Dependency injection: Properties and constructor args are injected.
  3. BeanNameAware / BeanFactoryAware: Callbacks if implemented.
  4. @PostConstruct / InitializingBean.afterPropertiesSet(): Post-initialization logic.
  5. Bean is ready — used by application.
  6. @PreDestroy / DisposableBean.destroy(): Cleanup on context close.
@Service
public class ProductCacheWarmer {
    @PostConstruct
    public void warmCache() {
        // runs after all dependencies are injected but before serving requests
    }

    @PreDestroy
    public void cleanup() {
        // runs before context shuts down
    }
}

Q6 — What is Spring Boot auto-configuration and how do you debug it? junior

Answer: Auto-configuration classes are loaded from META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. Each class is annotated with @ConditionalOn* annotations that check if the configuration should apply:

@AutoConfiguration
@ConditionalOnClass(RedisTemplate.class)   // only if Redis is on classpath
@ConditionalOnMissingBean(RedisTemplate.class) // only if no existing bean
public class RedisAutoConfiguration {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(...) { ... }
}

Debugging auto-configuration:

Override: Declare your own @Bean of the same type to prevent Spring Boot's auto-configured one from being created (due to @ConditionalOnMissingBean).


Q7 — What is application.yml and how do you bind properties to a Java class? junior

Answer: application.yml is the primary configuration file for Spring Boot applications. Properties are organized hierarchically and override Spring's defaults.

Binding to a POJO with @ConfigurationProperties:

app:
  order:
    max-items: 50
    timeout-seconds: 30
    payment-topic: order.created
@ConfigurationProperties(prefix = "app.order")
@Component
public class OrderProperties {
    private int maxItems;
    private int timeoutSeconds;
    private String paymentTopic;
    // getters/setters or use record
}

Benefits over @Value:


Q8 — What is @Transactional and how does Spring manage transactions? junior

Answer: @Transactional marks a method (or class) to run within a database transaction. Spring uses AOP proxies to intercept the method call, begin a transaction, execute the method, and commit or rollback based on the outcome.

@Service
public class OrderService {
    @Transactional
    public Order placeOrder(OrderRequest request) {
        Order order = orderRepo.save(new Order(...));
        outboxRepo.save(new OutboxEvent(order.getId(), ...)); // same transaction
        return order; // commit on return; rollback on unchecked exception
    }
}

Key properties:

This system: OrderService.placeOrder() is @Transactional to atomically write orders + outbox rows.


Q9 — What is Spring Boot Actuator and what endpoints does it expose? junior

Answer: Spring Boot Actuator provides production-ready features for monitoring and managing Spring Boot apps.

Key endpoints (exposed via HTTP at /actuator):

Endpoint Description
/actuator/health Application and component health (UP/DOWN)
/actuator/info Application info (git commit, build version)
/actuator/metrics Micrometer metrics (JVM, HTTP, custom)
/actuator/env Configuration properties and environment
/actuator/beans All Spring beans
/actuator/conditions Auto-configuration report
/actuator/loggers View and change log levels at runtime
/actuator/prometheus Prometheus scrape endpoint

Security: By default only /health and /info are web-exposed. Enable others:

management:
  endpoints:
    web:
      exposure:
        include: "health,info,metrics,prometheus"

This system: All services expose /actuator/health for Docker health checks.


Q10 — What is Spring Data JPA and how does it generate queries from method names? junior

Answer: Spring Data JPA provides repository abstractions over JPA. By extending JpaRepository<Entity, ID>, you get standard CRUD operations. Custom queries are derived from method names using a DSL.

public interface OrderRepository extends JpaRepository<Order, UUID> {
    // Generated: SELECT * FROM orders WHERE customer_id = ? AND status = ?
    List<Order> findByCustomerIdAndStatus(UUID customerId, OrderStatus status);

    // Generated: SELECT * FROM orders WHERE created_at > ? ORDER BY created_at DESC
    List<Order> findByCreatedAtAfterOrderByCreatedAtDesc(LocalDateTime since);

    // Custom JPQL for complex queries
    @Query("SELECT o FROM Order o WHERE o.status = 'PENDING' AND o.createdAt < :cutoff")
    List<Order> findStalePendingOrders(@Param("cutoff") LocalDateTime cutoff);
}

Method name keywords: findBy, countBy, existsBy, deleteBy + And, Or, Between, LessThan, GreaterThan, Like, OrderBy, Top, First.


Q11 — What is the N+1 query problem in JPA and how do you fix it? senior

Answer: The N+1 problem occurs when JPA fetches a collection lazily: 1 query to fetch N parent entities, then N more queries (one per parent) to fetch the child collection.

// PROBLEM: 1 query for orders + N queries for each order's items
List<Order> orders = orderRepo.findAll(); // 1 query
for (Order o : orders) {
    o.getItems().size(); // N lazy queries — one per order!
}

Solutions:

1. Fetch join (JPQL):

@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.status = :status")
List<Order> findWithItems(@Param("status") OrderStatus status);

2. @EntityGraph (declarative):

@EntityGraph(attributePaths = {"items", "items.product"})
List<Order> findByCustomerId(UUID customerId);

3. Batch fetching: @BatchSize(size=20) on the collection — fetches 20 children per query with IN clause.

Detection: Enable spring.jpa.show-sql=true and count SELECT statements, or use p6spy / Hibernate Statistics.


Q12 — What is the difference between @Bean and @Component? junior

Answer:

@Component @Bean
Applied to Class Method inside @Configuration class
Who creates instance Spring (via classpath scan) You write the factory method
Use when You own the class Third-party class or complex initialization needed

Example:

// @Component — you own the class
@Component
public class OrderValidator { ... }

// @Bean — configuring a third-party library object
@Configuration
public class KafkaConfig {
    @Bean
    public KafkaTemplate<String, OrderCreatedEvent> kafkaTemplate(ProducerFactory<String, OrderCreatedEvent> pf) {
        return new KafkaTemplate<>(pf);  // can't annotate KafkaTemplate itself
    }
}

Both result in Spring-managed singleton beans (by default). @Bean methods in @Configuration classes are proxied by CGLIB to ensure singleton semantics even when called directly.


Q13 — What is @Profile and how do you use it for environment-specific beans? junior

Answer: @Profile activates beans only when a specific profile is active. Use it to provide different implementations per environment.

@Service
@Profile("!test")
public class RealPaymentGateway implements PaymentGateway { ... }

@Service
@Profile("test")
public class MockPaymentGateway implements PaymentGateway { ... }

Activation:

Profile-specific properties: application-dev.yml overrides application.yml when dev profile is active.

This system: application-test.yml disables Kafka auto-startup and uses EmbeddedKafkaBroker in tests.


Q14 — How does Spring Boot handle externalized configuration and what is the property precedence order? senior

Answer: Spring Boot loads configuration from many sources. Priority (higher overrides lower):

  1. Command-line arguments (--server.port=9090)
  2. SPRING_APPLICATION_JSON env var (inline JSON)
  3. OS environment variables (SERVER_PORT=9090)
  4. JVM system properties (-Dserver.port=9090)
  5. application.properties / application.yml in current directory
  6. Profile-specific properties (application-prod.yml) in JAR
  7. Default application.properties / application.yml in JAR
  8. @PropertySource annotations
  9. Default values in @Value

Kubernetes/Docker: Inject sensitive config (DB passwords, API keys) via environment variables or Kubernetes Secrets — they override the application.yml defaults without modifying the JAR.

This system: DB credentials and Kafka bootstrap are injected via Docker Compose environment variables, overriding application.yml defaults.


Q15 — What is Spring AOP and how is it used in Spring Boot? senior

Answer: Aspect-Oriented Programming (AOP) allows cross-cutting concerns (logging, transactions, security, caching) to be modularized and applied declaratively without modifying business logic.

Key concepts:

Example — execution time logging:

@Aspect
@Component
public class PerformanceAspect {
    @Around("@annotation(com.ecommerce.Monitored)")
    public Object logTime(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        log.info("{} took {} ms", pjp.getSignature().getName(), System.currentTimeMillis() - start);
        return result;
    }
}

Spring uses AOP internally: @Transactional, @Cacheable, @Async, @Retryable, @Secured are all implemented as AOP aspects.

Limitation: Spring AOP only intercepts Spring-managed bean method calls. It cannot intercept private methods or calls within the same bean (self-invocation bypasses the proxy).


Q16 — What is @Cacheable and how does it work with Redis? senior

Answer: @Cacheable marks a method whose result should be cached. On first call, the method executes and the result is stored in the cache. On subsequent calls with the same key, the cached value is returned without executing the method.

@Service
public class ProductService {
    @Cacheable(value = "products", key = "#id", unless = "#result == null")
    public ProductDto getProduct(UUID id) {
        return productRepo.findById(id).map(this::toDto).orElse(null);
    }

    @CacheEvict(value = "products", key = "#product.id")
    public void updateProduct(Product product) {
        productRepo.save(product);
    }
}

Redis integration: Configure RedisCacheManager with TTL:

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofSeconds(600))
        .serializeValuesWith(RedisSerializationContext.SerializationPair
            .fromSerializer(new GenericJackson2JsonRedisSerializer()));
    return RedisCacheManager.builder(factory).cacheDefaults(config).build();
}

This system: Uses manual RedisTemplate instead of @Cacheable for finer control over serialization and the X-Cache header.


Q17 — What is @Async and what are its pitfalls? senior

Answer: @Async runs a method in a separate thread (thread pool) so the caller doesn't block.

@Async
@Transactional
public CompletableFuture<Void> sendNotificationEmail(String userId) {
    emailClient.send(...);
    return CompletableFuture.completedFuture(null);
}

Enable: @EnableAsync on a configuration class.

Pitfalls:

  1. Self-invocation: Calling an @Async method from within the same bean skips the proxy → runs synchronously.
  2. Transaction boundary: @Async creates a new thread outside the caller's transaction context. Use @Transactional on the async method itself with REQUIRES_NEW if persistence is needed.
  3. Exception handling: Exceptions in async void methods are swallowed unless you configure AsyncUncaughtExceptionHandler.
  4. Thread pool exhaustion: Default pool is SimpleAsyncTaskExecutor (creates new threads unboundedly). Always configure a ThreadPoolTaskExecutor with bounded queue.

Q18 — What is Micrometer and how does it integrate with Spring Boot metrics? senior

Answer: Micrometer is a metrics instrumentation library — a vendor-neutral facade over monitoring systems (Prometheus, Datadog, CloudWatch, etc.). Spring Boot auto-configures Micrometer and publishes JVM, HTTP, DB, and Kafka metrics.

Built-in metrics (exposed at /actuator/prometheus):

Custom metrics:

@Autowired
MeterRegistry registry;

Counter orderCounter = Counter.builder("orders.placed")
    .tag("status", "success")
    .register(registry);

orderCounter.increment();

This system: Uses Micrometer with Zipkin for distributed tracing. All services include spring-boot-starter-actuator and micrometer-tracing-bridge-otel.


Q19 — What is distributed tracing and how does Zipkin work with Spring Boot? senior

Answer: Distributed tracing tracks a request as it propagates across microservices. Each request is assigned a Trace ID; each service call within the trace is a Span with its own start/end timestamps and metadata.

Spring Boot integration:

management:
  tracing:
    sampling:
      probability: 1.0   # 100% in dev; use 0.1 (10%) in prod
spring:
  zipkin:
    base-url: http://zipkin:9411

How it works:

  1. API Gateway receives POST /orders → generates traceId=abc123, spanId=111.
  2. Forwards to order-service with traceparent: 00-abc123-111-01 header.
  3. order-service creates child span spanId=222, sends Kafka message with trace header.
  4. payment-service picks up trace from Kafka header, creates child span spanId=333.
  5. All spans visible in Zipkin UI as a timeline for traceId=abc123.

Q20 — How does Spring Boot Devtools improve developer productivity? junior

Answer: Spring Boot Devtools provides automatic application restart, LiveReload, and development-optimized defaults:

Auto-restart: When classpath files change (compiled classes), Devtools restarts the app using two class loaders — base classloader (unchanged dependencies) and restart classloader (your code). Restart is ~10× faster than a full cold start.

LiveReload: Triggers browser refresh on static resource changes.

Development-optimized defaults:

Exclusions: Devtools is automatically excluded from production JARs (<optional>true</optional> in POM) and doesn't activate when running from a fully packaged JAR.


Q21 — What is the difference between @RestController and @Controller? junior

Answer:

@RestController
@RequestMapping("/api/products")
public class ProductController {
    @GetMapping("/{id}")
    public ResponseEntity<ProductDto> getProduct(@PathVariable UUID id) {
        return ResponseEntity.ok(productService.getProduct(id));
    }
}

Without @ResponseBody (or @RestController), returning ProductDto would cause Spring to look for a view named productDto — failing with 404.


Q22 — What is ResponseEntity and when should you use it? junior

Answer: ResponseEntity<T> gives full control over the HTTP response: status code, headers, and body.

// Return 200 with body
return ResponseEntity.ok(product);

// Return 201 Created with Location header
return ResponseEntity.created(URI.create("/products/" + product.getId())).body(product);

// Return 404
return ResponseEntity.notFound().build();

// Custom headers
return ResponseEntity
    .ok()
    .header("X-Cache", "HIT")
    .body(product);

vs. returning T directly: Returning T directly implies 200 OK with default headers — fine for simple cases. Use ResponseEntity when you need custom status codes, headers (e.g., Location, X-Cache), or conditional responses.

This system: product-service returns ResponseEntity to set X-Cache: HIT/MISS header.


Q23 — What is @ControllerAdvice and how do you build a global exception handler? junior

Answer: @ControllerAdvice (or @RestControllerAdvice) defines cross-cutting exception handling for all controllers in one place, avoiding repeated try-catch blocks.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
        return new ErrorResponse("NOT_FOUND", ex.getMessage());
    }

    @ExceptionHandler(ValidationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidation(ValidationException ex) {
        return new ErrorResponse("VALIDATION_ERROR", ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleGeneric(Exception ex) {
        log.error("Unexpected error", ex);
        return new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred");
    }
}

Important: Never expose internal exception messages (stack traces, SQL errors) to clients — security risk (information disclosure). Log internally; return safe, generic messages externally.


Q24 — What is Spring Validation and how does Bean Validation work with Spring Boot? junior

Answer: Spring Boot integrates Jakarta Bean Validation (Hibernate Validator) for declarative input validation.

Annotate DTO:

public class CreateOrderRequest {
    @NotNull
    @Size(min = 1, max = 50)
    private List<@NotNull UUID> productIds;

    @Positive
    private int quantity;

    @Email
    private String customerEmail;
}

Enable in controller:

@PostMapping("/orders")
public ResponseEntity<Order> createOrder(@Valid @RequestBody CreateOrderRequest req) {
    ...
}

If validation fails, Spring throws MethodArgumentNotValidException → handle in @RestControllerAdvice to return 400 with field-level errors.

Custom validators: Implement ConstraintValidator<YourAnnotation, YourType> and annotate with @Constraint(validatedBy = ...).


Q25 — How does Spring Boot handle database migrations? senior

Answer: Spring Boot integrates Flyway and Liquibase for database schema migration management.

Flyway (used in this system):

-- V1__create_orders_table.sql
CREATE TABLE orders (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    customer_id UUID NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

-- V2__add_outbox_table.sql
CREATE TABLE outbox (
    id BIGSERIAL PRIMARY KEY,
    event_type VARCHAR(100) NOT NULL,
    payload TEXT NOT NULL,
    published BOOLEAN NOT NULL DEFAULT FALSE,
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

Benefits: Version-controlled schema changes, reproducible DB state across environments, CI/CD integration.


Q26 — What is Spring's event system and how does ApplicationEventPublisher work? senior

Answer: Spring has a built-in event bus for decoupled intra-process communication. Components publish events; listeners react without the publisher knowing who's listening.

Publish:

@Service
public class OrderService {
    @Autowired
    private ApplicationEventPublisher publisher;

    @Transactional
    public Order placeOrder(OrderRequest req) {
        Order order = orderRepo.save(new Order(req));
        publisher.publishEvent(new OrderPlacedEvent(this, order.getId()));
        return order;
    }
}

Listen:

@Component
public class OrderAuditListener {
    @EventListener
    @Async
    public void onOrderPlaced(OrderPlacedEvent event) {
        auditLog.record("Order placed: " + event.getOrderId());
    }
}

Transactional listeners: @TransactionalEventListener(phase = AFTER_COMMIT) runs only after the publishing transaction commits — prevents sending emails for orders that were rolled back.

vs. Kafka: ApplicationEvents are in-process only. Use Kafka for inter-service communication, Spring events for intra-service decoupling.


Q27 — How do you configure and tune the Spring Boot embedded Tomcat? senior

Answer: Embedded Tomcat is configured via server.* properties or programmatically via WebServerFactoryCustomizer.

Common tunings:

server:
  port: 8083
  tomcat:
    max-threads: 200           # max request handling threads (default 200)
    min-spare-threads: 10      # always-alive threads
    accept-count: 100          # queue size when all threads busy
    connection-timeout: 20000  # ms to wait for request data
    max-connections: 10000     # max concurrent TCP connections
  compression:
    enabled: true
    mime-types: application/json,text/plain
    min-response-size: 2048

Thread pool sizing:

This system: Default configuration. With Java 21 virtual threads enabled (spring.threads.virtual.enabled=true), Tomcat tasks use virtual threads — dramatically increasing concurrency.


Q28 — What is the difference between @Scheduled and a Quartz job in Spring? senior

Answer:

@Scheduled Quartz Scheduler
Setup @EnableScheduling + annotation Separate library, config, DB tables
Distributed No — runs on every instance Yes — Job store in DB, single execution
Persistence No — lost on restart Yes — jobs survive restarts
Clustering No Yes — built-in lock-based clustering
Cron support Yes Yes
Dynamic scheduling No — fixed at startup Yes — add/remove jobs at runtime

@Scheduled is ideal for simple, single-instance tasks:

@Scheduled(fixedDelay = 100)
public void publishOutboxEvents() {
    outboxPublisher.publishPendingEvents();
}

This system: Uses @Scheduled for OutboxPublisher (100 ms fixed delay). Acceptable for single-pod deployment. For multi-pod: add a Redis distributed lock to prevent duplicate publishing (covered in Redis Q9).


Q29 — How does Spring Security work and what is the filter chain? senior

Answer: Spring Security intercepts requests via a chain of SecurityFilterChain filters registered with the servlet container. Filters execute in order on every request.

Key filters (in order):

  1. SecurityContextPersistenceFilter: Loads security context from session.
  2. UsernamePasswordAuthenticationFilter: Processes form login.
  3. BearerTokenAuthenticationFilter: Extracts and validates JWT bearer tokens.
  4. ExceptionTranslationFilter: Converts AccessDeniedException → 403, AuthenticationException → 401.
  5. FilterSecurityInterceptor: Enforces authorization rules (hasRole, hasAuthority).

Configuration (Spring Security 6 / Spring Boot 3):

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .csrf(AbstractHttpConfigurer::disable)   // REST APIs use JWT, not cookies
        .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/auth/**").permitAll()
            .requestMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
        )
        .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
        .build();
}

This system: api-gateway uses Spring Cloud Gateway's SessionValidationFilter (custom WebFilter) to validate JWT via Redis, injecting user context headers downstream. Individual services trust X-User-Id headers (no re-validation needed).


Q30 — How do you write integration tests for a Spring Boot service with Testcontainers? senior

Answer: Testcontainers spins up real Docker containers for dependencies (PostgreSQL, Redis, Kafka) in tests, providing production-equivalent behavior without mocking.

@SpringBootTest(webEnvironment = RANDOM_PORT)
@Testcontainers
class OrderServiceIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
        .withDatabaseName("orders_test");

    @Container
    static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.1"));

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
        registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
    }

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldPlaceOrderAndPublishEvent() {
        ResponseEntity<OrderDto> response = restTemplate.postForEntity("/orders", new CreateOrderRequest(...), OrderDto.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        // ... await Kafka message
    }
}

@DynamicPropertySource: Injects container-specific connection details into Spring's environment, overriding application.yml at runtime.

This system: All per-service integration tests use this pattern. See e2e-tests module for full 14-container stack tests.