Spring Boot changed how Java developers build applications. Before Spring Boot, setting up a Spring project meant days of XML configuration, dependency version conflicts, and server deployment hassles. Now you can have a production-ready REST API running in minutes.
That convenience comes with hidden complexity. Spring Boot does so much automatically that many developers don't understand what's happening beneath the surface. Interviewers know this. They'll ask you to explain auto-configuration, describe the bean lifecycle, or troubleshoot why your @Transactional annotation isn't working. Surface-level knowledge gets exposed quickly.
This guide covers Spring Boot from fundamentals to production patterns - the concepts that separate developers who use Spring Boot from developers who understand it.
Spring Core Fundamentals
Before Spring Boot, there was Spring Framework. Understanding core Spring concepts is essential because Spring Boot builds directly on them.
Inversion of Control (IoC)
IoC is the foundational principle. Instead of your code controlling object creation, you invert that control to a container. The Spring IoC container creates objects, wires dependencies, and manages lifecycles.
// Without IoC: Your code controls dependencies
public class OrderService {
private PaymentService paymentService;
public OrderService() {
// You create the dependency - tight coupling
this.paymentService = new StripePaymentService();
}
}
// With IoC: Container controls dependencies
@Service
public class OrderService {
private final PaymentService paymentService;
// Container injects the dependency
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}Interview question: "Why is IoC beneficial?"
The answer goes beyond "loose coupling." IoC enables:
- Testability: Inject mock implementations for unit testing
- Flexibility: Swap implementations via configuration, not code changes
- Lifecycle management: Container handles creation, initialization, destruction
- AOP integration: Container can wrap beans with cross-cutting concerns
Dependency Injection Types
Spring supports three injection types. Know when to use each:
// Constructor injection (RECOMMENDED)
@Service
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
// All dependencies declared, immutable, testable
public OrderService(PaymentService paymentService,
InventoryService inventoryService) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
}
// Setter injection (optional dependencies)
@Service
public class NotificationService {
private EmailService emailService;
@Autowired(required = false)
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
}
// Field injection (AVOID in production code)
@Service
public class BadExample {
@Autowired
private PaymentService paymentService; // Hidden dependency, hard to test
}Interview question: "Why is constructor injection preferred?"
- Dependencies are explicit and documented in the constructor signature
- Objects are immutable after construction (final fields)
- Tests can instantiate the class directly without Spring context
- Prevents circular dependencies at compile time (mostly)
- Required dependencies are enforced - no NullPointerException surprises
Bean Lifecycle
Understanding the bean lifecycle helps you debug initialization issues and use lifecycle hooks correctly.
Container starts
↓
Bean instantiation (constructor called)
↓
Dependency injection (setters, fields)
↓
@PostConstruct method called
↓
InitializingBean.afterPropertiesSet() if implemented
↓
Custom init-method if specified
↓
Bean is ready for use
↓
... application runs ...
↓
@PreDestroy method called
↓
DisposableBean.destroy() if implemented
↓
Custom destroy-method if specified
↓
Container shuts down
@Service
public class CacheService {
private final CacheClient cacheClient;
public CacheService(CacheClient cacheClient) {
this.cacheClient = cacheClient;
// DON'T do heavy initialization here
// Dependencies might not be fully initialized
}
@PostConstruct
public void initialize() {
// Safe to use injected dependencies
cacheClient.connect();
cacheClient.warmUp();
}
@PreDestroy
public void cleanup() {
cacheClient.disconnect();
}
}@Component vs @Bean
Both create Spring-managed beans, but they serve different purposes:
// @Component: Annotate YOUR classes
@Component
public class MyService {
// Spring scans and registers this bean
}
// @Bean: Create beans from THIRD-PARTY classes or complex construction
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
// Can't annotate RestTemplate with @Component - it's not your code
RestTemplate template = new RestTemplate();
template.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
return template;
}
@Bean
public ObjectMapper objectMapper() {
// Complex configuration that doesn't belong in a constructor
return new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
}Interview question: "When would you use @Bean instead of @Component?"
- Third-party classes you can't annotate
- Conditional bean creation with complex logic
- Multiple beans of the same type with different configurations
- Beans requiring programmatic setup
Spring Boot Auto-Configuration
Auto-configuration is Spring Boot's killer feature - and one of the most misunderstood. Understanding how it works separates competent developers from those who just copy configurations.
How Auto-Configuration Works
When your application starts, Spring Boot:
- Scans
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsin all JARs - Evaluates
@Conditionalannotations on each auto-configuration class - Registers beans for configurations where conditions pass
// Simplified example of what auto-configuration looks like internally
@AutoConfiguration
@ConditionalOnClass(DataSource.class) // Only if DataSource is on classpath
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean // Only if no DataSource bean exists
public DataSource dataSource(DataSourceProperties properties) {
return DataSourceBuilder.create()
.url(properties.getUrl())
.username(properties.getUsername())
.password(properties.getPassword())
.build();
}
}The @Conditional annotations are key:
| Annotation | Condition |
|---|---|
@ConditionalOnClass | Class exists on classpath |
@ConditionalOnMissingClass | Class doesn't exist on classpath |
@ConditionalOnBean | Bean of type exists in context |
@ConditionalOnMissingBean | No bean of type exists |
@ConditionalOnProperty | Property has specific value |
@ConditionalOnWebApplication | Running as web application |
Starter Dependencies
Starters are curated dependency sets that trigger auto-configuration:
<!-- This single dependency brings in: -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--
- spring-webmvc (Spring MVC)
- spring-boot-starter-tomcat (embedded server)
- spring-boot-starter-json (Jackson)
- spring-boot-starter (core + logging)
And auto-configures:
- DispatcherServlet
- Error handling
- HTTP message converters
- Static resource handling
-->Interview question: "You add spring-boot-starter-data-jpa but your application fails to start with 'no DataSource configured.' Why?"
The JPA starter brings in DataSource auto-configuration, but auto-configuration needs connection details. Without spring.datasource.url in properties, the @ConditionalOnProperty conditions fail.
Customizing Auto-Configuration
Three ways to customize, from simple to advanced:
1. Properties (most common)
# application.properties
server.port=8081
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.jpa.hibernate.ddl-auto=validate2. Define your own bean (overrides auto-configured bean)
@Configuration
public class CustomConfig {
@Bean
public ObjectMapper objectMapper() {
// Auto-configuration sees this bean exists
// and skips its own ObjectMapper creation
return new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
}3. Exclude auto-configuration entirely
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
SecurityAutoConfiguration.class
})
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}Debugging Auto-Configuration
When things don't work, enable the debug report:
debug=trueThis prints a conditions evaluation report showing:
- Positive matches: What was auto-configured and why
- Negative matches: What wasn't configured and why
- Exclusions: What you explicitly excluded
REST API Development
Spring Boot makes REST API development straightforward, but interviews probe deeper than basic CRUD operations.
Controller Fundamentals
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<UserDTO> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return userService.findAll(PageRequest.of(page, size));
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<UserDTO> createUser(
@Valid @RequestBody CreateUserRequest request) {
UserDTO created = userService.create(request);
URI location = URI.create("/api/users/" + created.getId());
return ResponseEntity.created(location).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return userService.update(id, request)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
if (userService.delete(id)) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
}
}Interview question: "What's the difference between @Controller and @RestController?"
@RestController = @Controller + @ResponseBody. Every method in a @RestController automatically serializes return values to the response body (typically JSON). With plain @Controller, you'd need @ResponseBody on each method or return ModelAndView for templates.
Request Validation
Spring Boot integrates with Jakarta Bean Validation (formerly javax.validation):
public class CreateUserRequest {
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be 2-100 characters")
private String name;
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
@Pattern(regexp = ".*[A-Z].*", message = "Password must contain uppercase letter")
private String password;
// getters, setters
}The @Valid annotation on the controller parameter triggers validation. If validation fails, Spring throws MethodArgumentNotValidException.
Global Exception Handling
Production APIs need consistent error responses. Use @ControllerAdvice:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(
MethodArgumentNotValidException ex) {
List<FieldError> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> new FieldError(
error.getField(),
error.getDefaultMessage()))
.toList();
ErrorResponse response = new ErrorResponse(
"VALIDATION_ERROR",
"Request validation failed",
fieldErrors
);
return ResponseEntity.badRequest().body(response);
}
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
ResourceNotFoundException ex) {
ErrorResponse response = new ErrorResponse(
"NOT_FOUND",
ex.getMessage(),
null
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(Exception ex) {
// Log the full exception for debugging
log.error("Unexpected error", ex);
// Return generic message to client (don't leak internals)
ErrorResponse response = new ErrorResponse(
"INTERNAL_ERROR",
"An unexpected error occurred",
null
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
}
}ResponseEntity Best Practices
Interview question: "When would you return ResponseEntity vs just the object?"
Return the object directly when you always return 200 OK:
@GetMapping("/health")
public HealthStatus health() {
return new HealthStatus("UP"); // Always 200
}Use ResponseEntity when status codes vary:
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return userRepository.findById(id)
.map(ResponseEntity::ok) // 200 if found
.orElse(ResponseEntity.notFound().build()); // 404 if not
}
@PostMapping
public ResponseEntity<User> create(@RequestBody User user) {
User saved = userRepository.save(user);
URI location = URI.create("/users/" + saved.getId());
return ResponseEntity.created(location).body(saved); // 201 with Location header
}Data Access with Spring Data
Spring Data JPA eliminates boilerplate for database operations. But understanding what happens behind the scenes is crucial for performance and debugging.
Repository Pattern
// Basic repository - you get CRUD for free
public interface UserRepository extends JpaRepository<User, Long> {
// No implementation needed for basic operations
}
// Query methods - Spring generates queries from method names
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByStatus(UserStatus status);
List<User> findByCreatedAtAfter(LocalDateTime date);
List<User> findByNameContainingIgnoreCase(String namePart);
// Combining conditions
List<User> findByStatusAndCreatedAtAfter(
UserStatus status,
LocalDateTime date
);
// Limiting results
List<User> findTop10ByOrderByCreatedAtDesc();
// Counting
long countByStatus(UserStatus status);
// Existence check
boolean existsByEmail(String email);
}Interview question: "How does Spring Data generate the query from findByEmailAndStatus?"
Spring Data parses the method name:
find...Byindicates a SELECT queryEmailmaps to theemailpropertyAndcombines conditionsStatusmaps to thestatusproperty
It generates: SELECT u FROM User u WHERE u.email = ?1 AND u.status = ?2
Custom Queries
When method names get unwieldy, use @Query:
public interface OrderRepository extends JpaRepository<Order, Long> {
// JPQL query
@Query("SELECT o FROM Order o WHERE o.user.id = :userId AND o.status = :status")
List<Order> findUserOrdersByStatus(
@Param("userId") Long userId,
@Param("status") OrderStatus status
);
// Native SQL when you need database-specific features
@Query(value = """
SELECT * FROM orders o
WHERE o.created_at >= NOW() - INTERVAL '30 days'
AND o.total > :minTotal
ORDER BY o.total DESC
""", nativeQuery = true)
List<Order> findRecentHighValueOrders(@Param("minTotal") BigDecimal minTotal);
// Modifying queries
@Modifying
@Query("UPDATE Order o SET o.status = :status WHERE o.id IN :ids")
int updateStatusForOrders(
@Param("status") OrderStatus status,
@Param("ids") List<Long> ids
);
}Transaction Management
@Transactional is deceptively simple but frequently misunderstood:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
private final PaymentService paymentService;
@Transactional
public Order createOrder(CreateOrderRequest request) {
// All of this happens in one transaction
Order order = new Order(request);
// Reserve inventory
inventoryService.reserve(order.getItems());
// Process payment
paymentService.charge(order.getTotal());
// Save order
return orderRepository.save(order);
// If anything throws, everything rolls back
}
}Interview question: "Your @Transactional method calls another @Transactional method in the same class. Does the inner method get its own transaction?"
No. Spring's @Transactional uses proxies. When you call a method within the same class, you bypass the proxy - the annotation has no effect. This is a common bug.
@Service
public class OrderService {
@Transactional
public void processOrders(List<Long> orderIds) {
for (Long id : orderIds) {
// BUG: This call bypasses the proxy
// Each order does NOT get its own transaction
processOrder(id);
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processOrder(Long orderId) {
// This annotation is ignored when called from within the class
}
}Solutions:
- Inject the service into itself (ugly but works)
- Extract to a separate service class (cleaner)
- Use
TransactionTemplatefor programmatic control
N+1 Query Problem
The most common JPA performance issue:
// Entity with lazy relationship
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
private User user;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
}
// This code triggers N+1
@Transactional(readOnly = true)
public void printOrders() {
List<Order> orders = orderRepository.findAll(); // 1 query
for (Order order : orders) {
System.out.println(order.getUser().getName()); // N queries!
}
}Solutions:
// JOIN FETCH in JPQL
@Query("SELECT o FROM Order o JOIN FETCH o.user WHERE o.status = :status")
List<Order> findByStatusWithUser(@Param("status") OrderStatus status);
// EntityGraph
@EntityGraph(attributePaths = {"user", "items"})
List<Order> findByStatus(OrderStatus status);Security with Spring Security
Spring Security is powerful but complex. Interviews focus on understanding the architecture, not memorizing configurations.
Security Filter Chain
Spring Security works through a filter chain. Every request passes through filters that handle authentication and authorization:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
return http
.csrf(csrf -> csrf.disable()) // Disable for REST APIs with JWT
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}Interview question: "What's the order of security filter evaluation?"
Filters execute in order. For authorization rules, the first matching rule wins. In the example above:
/api/auth/loginmatches first rule - permitted/api/admin/usersmatches second rule - requires ADMIN roleGET /api/productsmatches third rule - permittedPOST /api/productsdoesn't match third rule, falls to fourth - requires authentication
JWT Authentication
Modern REST APIs typically use JWT:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
String username = jwtService.extractUsername(token);
if (username != null &&
SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService
.loadUserByUsername(username);
if (jwtService.isTokenValid(token, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource()
.buildDetails(request)
);
SecurityContextHolder.getContext()
.setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}Method-Level Security
For fine-grained control:
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
}
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteOrder(Long orderId) {
// Only admins can delete
}
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
public List<Order> getOrdersForUser(Long userId) {
// Users can only see their own orders (unless admin)
}
@PostAuthorize("returnObject.user.id == authentication.principal.id")
public Order getOrder(Long orderId) {
// Check after fetching - user can only see their own order
}
}Testing in Spring Boot
Spring Boot provides excellent testing support, but knowing which tools to use for each scenario is key.
Test Slices
Test slices load only the parts of the context you need:
// @WebMvcTest - Controllers only, mocks services
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void shouldReturnUser() throws Exception {
UserDTO user = new UserDTO(1L, "john@example.com", "John");
when(userService.findById(1L)).thenReturn(Optional.of(user));
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value("john@example.com"));
}
@Test
void shouldReturn404WhenUserNotFound() throws Exception {
when(userService.findById(999L)).thenReturn(Optional.empty());
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
}
@Test
void shouldValidateCreateRequest() throws Exception {
String invalidRequest = """
{
"email": "invalid-email",
"name": ""
}
""";
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(invalidRequest))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors").isArray());
}
}// @DataJpaTest - Repositories only, embedded database
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldFindByEmail() {
User user = new User("john@example.com", "John");
entityManager.persistAndFlush(user);
Optional<User> found = userRepository.findByEmail("john@example.com");
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("John");
}
@Test
void shouldReturnEmptyForNonexistentEmail() {
Optional<User> found = userRepository.findByEmail("nobody@example.com");
assertThat(found).isEmpty();
}
}Integration Tests
When you need the full context:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private OrderRepository orderRepository;
@Test
void shouldCreateAndRetrieveOrder() {
CreateOrderRequest request = new CreateOrderRequest(
List.of(new OrderItemRequest(1L, 2))
);
ResponseEntity<OrderDTO> createResponse = restTemplate
.postForEntity("/api/orders", request, OrderDTO.class);
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
Long orderId = createResponse.getBody().getId();
ResponseEntity<OrderDTO> getResponse = restTemplate
.getForEntity("/api/orders/" + orderId, OrderDTO.class);
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody().getItems()).hasSize(1);
}
@AfterEach
void cleanup() {
orderRepository.deleteAll();
}
}Testing with Testcontainers
For tests that need real databases:
@SpringBootTest
@Testcontainers
class UserServiceIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@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);
}
@Autowired
private UserService userService;
@Test
void shouldPersistUserToRealDatabase() {
UserDTO created = userService.create(
new CreateUserRequest("test@example.com", "Test User", "password123")
);
assertThat(created.getId()).isNotNull();
Optional<UserDTO> retrieved = userService.findById(created.getId());
assertThat(retrieved).isPresent();
}
}Production Readiness
Spring Boot includes features specifically for production deployments.
Actuator Endpoints
Spring Boot Actuator provides operational information:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency># Expose specific endpoints
management.endpoints.web.exposure.include=health,info,metrics,prometheus
# Health endpoint details
management.endpoint.health.show-details=when_authorized
# Custom health indicators contribute to overall health
management.health.diskspace.enabled=true
management.health.db.enabled=trueKey endpoints:
/actuator/health- Application health status/actuator/info- Application information/actuator/metrics- Application metrics/actuator/prometheus- Prometheus-format metrics
Custom Health Indicators
@Component
public class ExternalServiceHealthIndicator implements HealthIndicator {
private final ExternalServiceClient client;
@Override
public Health health() {
try {
client.ping();
return Health.up()
.withDetail("service", "external-api")
.withDetail("status", "reachable")
.build();
} catch (Exception e) {
return Health.down()
.withDetail("service", "external-api")
.withDetail("error", e.getMessage())
.build();
}
}
}Configuration Management
Profiles for environment-specific configuration:
# application.properties (defaults)
spring.datasource.url=jdbc:h2:mem:devdb
# application-prod.properties
spring.datasource.url=jdbc:postgresql://prod-db:5432/app
spring.jpa.hibernate.ddl-auto=validate
# application-test.properties
spring.datasource.url=jdbc:h2:mem:testdbActivate profiles: --spring.profiles.active=prod
Externalized configuration for sensitive values:
# Reference environment variables
spring.datasource.password=${DB_PASSWORD}
# With default fallback
server.port=${PORT:8080}Logging
Spring Boot uses Logback by default:
# Root level
logging.level.root=INFO
# Package-specific levels
logging.level.com.myapp=DEBUG
logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate.SQL=DEBUG
# Log to file
logging.file.name=/var/log/myapp/application.log
logging.file.max-size=10MB
logging.file.max-history=30Structured logging for production (add logstash-encoder):
@Slf4j
@Service
public class OrderService {
public Order createOrder(CreateOrderRequest request) {
log.info("Creating order",
kv("userId", request.getUserId()),
kv("itemCount", request.getItems().size()));
// ... create order
log.info("Order created",
kv("orderId", order.getId()),
kv("total", order.getTotal()));
return order;
}
}Common Interview Questions
Q: What happens when you start a Spring Boot application?
main()callsSpringApplication.run()- Spring creates the
ApplicationContext - Component scanning finds
@Componentclasses - Auto-configuration runs based on classpath and conditions
- Beans are instantiated and dependencies injected
@PostConstructmethods executeApplicationRunner/CommandLineRunnerbeans run- Application is ready to handle requests
Q: How do you handle database migrations in Spring Boot?
Use Flyway or Liquibase. Spring Boot auto-configures them:
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migrationQ: What's the difference between @SpringBootTest and @WebMvcTest?
@SpringBootTest loads the full application context - use for integration tests. @WebMvcTest loads only web layer (controllers, filters) - use for fast, focused controller tests with mocked services.
Q: How do you implement caching in Spring Boot?
Enable caching and annotate methods:
@EnableCaching
@SpringBootApplication
public class Application { }
@Service
public class ProductService {
@Cacheable("products")
public Product findById(Long id) {
// Only called on cache miss
return repository.findById(id);
}
@CacheEvict(value = "products", key = "#product.id")
public Product update(Product product) {
return repository.save(product);
}
}Related Resources
- Java Core Interview Guide - OOP, collections, streams, concurrency
- REST API Interview Guide - API design principles
- PostgreSQL & Node.js Interview Guide - Database concepts (applicable to Java)
- Authentication & JWT Interview Guide - Security fundamentals
- Testing Strategies Interview Guide - Testing approaches
