Spring Boot Interview — The Complete Guide
Everything you need for Spring Boot rounds at product companies. Core internals, auto-configuration, JPA deep dive, security, testing strategies, microservices patterns, and the questions that actually get asked.
Table of Contents
Spring Core & IoC Container
Spring's foundation is the IoC (Inversion of Control) container. Instead of your code creating dependencies with new, the container creates and wires them for you. Understanding this deeply is the difference between using Spring and understanding Spring.
🔥 Inversion of Control — The Core Idea
Traditional code: OrderService creates its own OrderRepository. With IoC: the container creates both and injects the repository into the service. The "inversion" is that the framework controls object creation, not your code. This enables loose coupling, testability (inject mocks), and lifecycle management.
// ❌ Without IoC — tight coupling
public class OrderService {
private final OrderRepository repo = new OrderRepository(); // creates its own dependency
}
// ✅ With IoC — container injects the dependency
@Service
public class OrderService {
private final OrderRepository repo;
public OrderService(OrderRepository repo) {
this.repo = repo; // injected by Spring
}
}
🔥 ApplicationContext vs BeanFactory
| Aspect | BeanFactory | ApplicationContext |
|---|---|---|
| Loading | Lazy — creates beans on first request | Eager — creates all singleton beans at startup |
| Features | Basic DI only | DI + events, i18n, AOP, resource loading |
| Use case | Memory-constrained environments (rare) | 99% of applications — always use this |
| Startup | Faster (lazy) | Slower but catches config errors early |
💡 Interview answer
"ApplicationContext is the superset. It extends BeanFactory and adds enterprise features like event publishing, internationalization, and AOP support. In practice, you always use ApplicationContext — BeanFactory is only relevant for extremely resource-constrained environments."
🔥 @Component vs @Bean — Must Know
Both register beans in the container, but they work differently. @Component (and its specializations) is a class-level annotation — Spring discovers it via component scanning. @Bean is a method-level annotation inside a @Configuration class — you manually define the bean creation logic. Use @Bean when you need to configure third-party classes you can't annotate.
// @Component — Spring discovers and creates this automatically
@Service // specialization of @Component
public class UserService {
// ...
}
// @Bean — you control instantiation (useful for third-party classes)
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(10))
.build();
}
}
Stereotype Annotations
| Annotation | Layer | Extra behavior |
|---|---|---|
| @Component | Generic | Base annotation. No extra behavior. |
| @Service | Business logic | Semantic only — no extra behavior over @Component. |
| @Repository | Data access | Translates persistence exceptions to Spring's DataAccessException. |
| @Controller | Web layer | Enables request mapping. Returns views. |
| @RestController | Web layer (API) | @Controller + @ResponseBody. Returns JSON/XML directly. |
🔥 Bean Lifecycle
Understanding the bean lifecycle is critical for debugging initialization issues and managing resources. The full sequence:
- Instantiation — constructor called
- Populate properties — dependencies injected
@PostConstruct— initialization logic runsInitializingBean.afterPropertiesSet()— if implemented- Custom init method — if specified in
@Bean(initMethod) - Bean is ready to use
@PreDestroy— cleanup logic runs (on shutdown)DisposableBean.destroy()— if implemented
@Component
public class CacheWarmer {
@PostConstruct
public void warmCache() {
// Runs after DI is complete — load frequently accessed data
log.info("Warming cache...");
cacheService.loadHotData();
}
@PreDestroy
public void cleanup() {
// Runs on application shutdown — release resources
log.info("Flushing cache to disk...");
cacheService.flush();
}
}
Spring Profiles
Profiles let you swap beans and configuration per environment. Activate with spring.profiles.active=dev in properties or -Dspring.profiles.active=prod as a JVM argument. Use application-dev.yml and application-prod.yml for environment-specific config.
// Different implementations per environment
@Profile("dev")
@Service
public class MockPaymentGateway implements PaymentGateway {
public PaymentResult charge(Money amount) {
return PaymentResult.success(); // always succeeds in dev
}
}
@Profile("prod")
@Service
public class StripePaymentGateway implements PaymentGateway {
public PaymentResult charge(Money amount) {
return stripeClient.charge(amount); // real payment in prod
}
}
📝 Quick Revision
Quick Revision Cheat Sheet
IoC: Container creates and wires objects. You don't use 'new' for managed beans.
ApplicationContext: Use this always. Eager loading, events, AOP, i18n. Superset of BeanFactory.
@Component vs @Bean: @Component = class-level, auto-scanned. @Bean = method-level, manual config.
Stereotypes: @Service (logic), @Repository (data + exception translation), @Controller (web).
Lifecycle: Constructor → DI → @PostConstruct → ready → @PreDestroy → destroyed.
Profiles: Swap beans per env. application-{profile}.yml for config.
Common Interview Questions
Q:What is Inversion of Control and how does Spring implement it?
A: IoC means the framework controls object creation and wiring, not your code. Spring implements it through the ApplicationContext container — you declare beans (via annotations or @Bean methods), and Spring creates them, resolves dependencies, and manages their lifecycle. The key benefit: loose coupling and testability.
Q:What's the difference between @Component, @Service, @Repository, and @Controller?
A: All are stereotype annotations that register beans. @Component is the base. @Service is semantic (no extra behavior). @Repository adds persistence exception translation. @Controller enables request mapping for MVC. @RestController adds @ResponseBody for API responses. Use the right one for the right layer.
Auto-Configuration
Auto-configuration is what makes Spring Boot "opinionated." Add a dependency to your classpath, and Spring Boot automatically configures the beans you need. No XML, no manual setup. Understanding how this works internally separates Boot users from Boot experts.
🔥 How Auto-Configuration Works Internally
When you annotate your main class with @SpringBootApplication, it triggers three things: (1) @Configuration — marks it as a bean source, (2) @EnableAutoConfiguration — loads auto-config classes, (3) @ComponentScan — scans the package tree for beans.
The auto-config classes are listed in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (Spring Boot 3.x) or META-INF/spring.factories (2.x). Spring loads all of them, but each class uses @Conditional annotations to decide whether to activate.
🔥 @Conditional Annotations — The Decision Engine
| Annotation | Activates when... | Example |
|---|---|---|
| @ConditionalOnClass | A class is on the classpath | DataSource.class → configure DB |
| @ConditionalOnMissingBean | No bean of that type exists | Don't override user's custom bean |
| @ConditionalOnProperty | A property has a specific value | spring.cache.type=redis → configure Redis cache |
| @ConditionalOnBean | A specific bean already exists | Configure JPA only if DataSource bean exists |
| @ConditionalOnMissingClass | A class is NOT on the classpath | Fallback configuration |
| @ConditionalOnWebApplication | Running as a web app | Configure DispatcherServlet |
// How Spring Boot auto-configures a DataSource
@AutoConfiguration
@ConditionalOnClass(DataSource.class) // only if JDBC is on classpath
@ConditionalOnProperty(name = "spring.datasource.url") // only if URL is configured
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean // only if user hasn't defined their own
public DataSource dataSource(DataSourceProperties props) {
return DataSourceBuilder.create()
.url(props.getUrl())
.username(props.getUsername())
.password(props.getPassword())
.build();
}
}
Key insight
@ConditionalOnMissingBean is the escape hatch. If you define your own DataSource bean, Spring Boot's auto-configured one backs off. This is how you override defaults without fighting the framework.
Starter Dependencies
Starters are curated dependency bundles. spring-boot-starter-web pulls in Tomcat, Spring MVC, Jackson, and validation. spring-boot-starter-data-jpa pulls in Hibernate, Spring Data JPA, and HikariCP. You don't pick individual libraries — the starter gives you a tested, compatible set.
application.yml vs application.properties
| Aspect | application.properties | application.yml |
|---|---|---|
| Format | Key=value pairs | YAML hierarchy |
| Readability | Flat — repetitive prefixes | Nested — cleaner for deep config |
| Multi-profile | Separate files per profile | Can use --- separator in one file |
| Preference | Simpler for small configs | Better for complex, nested configs |
Debugging Auto-Configuration
Set debug=true in application.properties to see the auto-configuration report at startup. It shows which configs were applied (positive matches) and which were skipped (negative matches) with the reason. This is your first tool when something isn't being configured as expected.
📝 Quick Revision
Quick Revision Cheat Sheet
@SpringBootApplication: @Configuration + @EnableAutoConfiguration + @ComponentScan.
Auto-config: Reads classpath + properties. Uses @Conditional to decide what to configure.
@ConditionalOnMissingBean: The escape hatch. Define your own bean to override auto-config.
Starters: Curated dependency bundles. Tested compatible versions.
Debug: debug=true shows which auto-configs matched and why.
Dependency Injection Deep Dive
DI is the mechanism that implements IoC. Spring supports three injection styles, but only one is recommended. Interviewers test whether you know the trade-offs and can handle edge cases like circular dependencies and ambiguous beans.
🔥 Constructor vs Field vs Setter Injection
| Style | How | Pros | Cons |
|---|---|---|---|
| Constructor | Dependencies as constructor params | Immutable (final), testable, fails fast, explicit | Verbose with many deps (but that's a design smell) |
| Field (@Autowired) | @Autowired on private field | Less boilerplate | Can't be final, hidden deps, hard to test, reflection-based |
| Setter | @Autowired on setter method | Optional deps, reconfigurable | Mutable, easy to forget to call setter |
🔥 The official recommendation
Spring team recommends constructor injection. Since Spring 4.3, if a class has only one constructor, @Autowired is optional — Spring auto-injects. This is the cleanest pattern.
// ✅ Constructor injection (preferred)
@Service
public class OrderService {
private final OrderRepository repo;
private final NotificationService notifications;
// @Autowired is optional with single constructor (Spring 4.3+)
public OrderService(OrderRepository repo, NotificationService notifications) {
this.repo = repo;
this.notifications = notifications;
}
}
// ❌ Field injection (avoid)
@Service
public class OrderService {
@Autowired private OrderRepository repo; // not final, hidden dependency
@Autowired private NotificationService notifications; // can't easily mock in tests
}
🔥 @Qualifier and @Primary — Resolving Ambiguity
When multiple beans implement the same interface, Spring doesn't know which to inject. @Primary marks a default choice. @Qualifier lets you pick a specific one by name. @Qualifier overrides @Primary.
public interface NotificationService {
void send(String message);
}
@Service
@Primary // default when no qualifier specified
public class EmailNotificationService implements NotificationService { ... }
@Service("smsNotifier")
public class SmsNotificationService implements NotificationService { ... }
// Uses EmailNotificationService (the @Primary)
@Service
public class OrderService {
public OrderService(NotificationService notifications) { ... }
}
// Uses SmsNotificationService (explicit @Qualifier)
@Service
public class AlertService {
public AlertService(@Qualifier("smsNotifier") NotificationService notifications) { ... }
}
🔥 Bean Scopes
| Scope | Instances | When to use |
|---|---|---|
| singleton (default) | One per ApplicationContext | Stateless services, repositories, configs — 95% of beans |
| prototype | New instance per injection/request | Stateful beans, builders, non-thread-safe objects |
| request | One per HTTP request | Request-scoped data (user context, request metadata) |
| session | One per HTTP session | User session data (shopping cart in traditional web apps) |
| application | One per ServletContext | Shared across all sessions — similar to singleton |
💡 Gotcha: Injecting prototype into singleton
If you inject a prototype-scoped bean into a singleton, the prototype is created once and reused — defeating the purpose. Fix: inject ObjectProvider<T> or Provider<T> and call .getObject() each time you need a new instance.
🔥 Circular Dependencies
A depends on B, B depends on A. With constructor injection, Spring fails fast at startup — it can't create either bean. This is a good thing — it forces you to fix the design. Solutions:
- Redesign — extract shared logic into a third service
- Use
@Lazyon one dependency — creates a proxy, breaks the cycle - Use setter injection on one side (not recommended — hides the problem)
- Use events — A publishes an event, B listens (decoupled)
// ❌ Circular dependency — fails at startup with constructor injection
@Service
public class ServiceA {
public ServiceA(ServiceB b) { }
}
@Service
public class ServiceB {
public ServiceB(ServiceA a) { }
}
// ✅ Fix 1: @Lazy — creates a proxy, resolves lazily
@Service
public class ServiceA {
public ServiceA(@Lazy ServiceB b) { }
}
// ✅ Fix 2 (better): Redesign — extract shared logic
@Service
public class SharedLogic { }
@Service
public class ServiceA {
public ServiceA(SharedLogic shared) { }
}
@Service
public class ServiceB {
public ServiceB(SharedLogic shared) { }
}
📝 Quick Revision
Quick Revision Cheat Sheet
Constructor injection: Preferred. Immutable, testable, fails fast. @Autowired optional with single constructor.
@Primary: Default bean when multiple candidates. @Qualifier overrides it.
Singleton scope: Default. One instance per container. Use for stateless services.
Prototype scope: New instance per injection. Use ObjectProvider to get fresh instances in singletons.
Circular deps: Redesign first. @Lazy as escape hatch. Events for decoupling.
REST API Design with Spring
Spring MVC is the web layer of Spring Boot. Every HTTP request flows through the DispatcherServlet, which routes it to the right controller method. Understanding this flow and the annotations that control it is essential.
🔥 Request Handling Flow
Client → Filter chain (servlet-level: CORS, logging, security) → DispatcherServlet → HandlerMapping (finds the controller method) → HandlerAdapter (invokes it) → Controller → response serialized by HttpMessageConverter (Jackson for JSON). Interceptors run between DispatcherServlet and the controller.
@RestController vs @Controller
| Annotation | Returns | Use case |
|---|---|---|
| @Controller | View name (resolved by ViewResolver) | Server-rendered HTML (Thymeleaf, JSP) |
| @RestController | Object → serialized to JSON/XML | REST APIs — 99% of modern Spring Boot apps |
@RestController = @Controller + @ResponseBody. Every method's return value is automatically serialized to JSON by Jackson.
🔥 Request Mapping Annotations
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@GetMapping // GET /api/v1/users
public List<UserDto> listUsers(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return userService.findAll(page, size);
}
@GetMapping("/{id}") // GET /api/v1/users/123
public UserDto getUser(@PathVariable Long id) {
return userService.findById(id);
}
@PostMapping // POST /api/v1/users
@ResponseStatus(HttpStatus.CREATED)
public UserDto createUser(@Valid @RequestBody CreateUserRequest request) {
return userService.create(request);
}
@PutMapping("/{id}") // PUT /api/v1/users/123
public UserDto updateUser(@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return userService.update(id, request);
}
@DeleteMapping("/{id}") // DELETE /api/v1/users/123
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
}
@PathVariable vs @RequestParam vs @RequestBody
| Annotation | Source | Example |
|---|---|---|
| @PathVariable | URL path segment | /users/{id} → @PathVariable Long id |
| @RequestParam | Query string | /users?page=2 → @RequestParam int page |
| @RequestBody | Request body (JSON) | POST body → @RequestBody CreateUserRequest req |
| @RequestHeader | HTTP header | Authorization header → @RequestHeader String auth |
🔥 Bean Validation (@Valid)
Spring integrates with Jakarta Bean Validation. Annotate DTO fields with constraints, add @Valid on the controller parameter, and Spring automatically validates before the method executes. Invalid input throws MethodArgumentNotValidException (400).
public record CreateUserRequest(
@NotBlank(message = "Name is required")
String name,
@Email(message = "Must be a valid email")
@NotBlank
String email,
@Size(min = 8, max = 100, message = "Password must be 8-100 characters")
String password,
@Min(value = 18, message = "Must be at least 18")
Integer age
) {}
// Custom validator
@Constraint(validatedBy = UniqueEmailValidator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueEmail {
String message() default "Email already exists";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Content Negotiation
Spring uses HttpMessageConverter to serialize/deserialize request and response bodies. Jackson (MappingJackson2HttpMessageConverter) handles JSON by default. You can support XML by adding jackson-dataformat-xml to the classpath. The Accept header determines the response format.
📝 Quick Revision
Quick Revision Cheat Sheet
Request flow: Filter → DispatcherServlet → HandlerMapping → Controller → MessageConverter → Response.
@RestController: @Controller + @ResponseBody. Returns JSON directly.
@Valid: Triggers Bean Validation on @RequestBody. Throws 400 on failure.
@PathVariable: From URL path. @RequestParam from query string. @RequestBody from JSON body.
ResponseEntity: Full control over status code, headers, and body. Use for custom responses.
Exception Handling
Clean error handling separates production APIs from tutorial code. Spring provides a layered approach: controller-level, global, and framework-level exception handling.
🔥 @ControllerAdvice + @ExceptionHandler
@RestControllerAdvice is a global exception handler that catches exceptions thrown by any controller. Each @ExceptionHandler method handles a specific exception type. This centralizes error handling — no try-catch in controllers.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
var error = new ErrorResponse("NOT_FOUND", ex.getMessage());
return ResponseEntity.status(404).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
List<FieldError> errors = ex.getBindingResult().getFieldErrors().stream()
.map(f -> new FieldError(f.getField(), f.getDefaultMessage()))
.toList();
var error = new ErrorResponse("VALIDATION_ERROR", "Invalid input", errors);
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<ErrorResponse> handleConflict(DataIntegrityViolationException ex) {
var error = new ErrorResponse("CONFLICT", "Resource already exists");
return ResponseEntity.status(409).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
log.error("Unhandled exception", ex);
var error = new ErrorResponse("INTERNAL_ERROR", "Something went wrong");
return ResponseEntity.internalServerError().body(error);
}
}
Consistent Error Response Shape
// Always return the same shape — clients can parse predictably
public record ErrorResponse(
String code,
String message,
List<FieldError> details, // null for non-validation errors
Instant timestamp,
String requestId
) {
public ErrorResponse(String code, String message) {
this(code, message, null, Instant.now(), MDC.get("requestId"));
}
public ErrorResponse(String code, String message, List<FieldError> details) {
this(code, message, details, Instant.now(), MDC.get("requestId"));
}
}
public record FieldError(String field, String message) {}
Problem Detail (RFC 7807)
Spring Boot 3 supports RFC 7807 Problem Detail natively. Instead of custom error shapes, you can return standardized problem responses with type, title, status, detail, and instance fields. Enable with spring.mvc.problemdetails.enabled=true.
ResponseStatusException
For simple cases, throw ResponseStatusException directly from service or controller code without needing a custom exception class. Useful for quick prototyping, but @ControllerAdvice is better for production — it centralizes handling and keeps controllers clean.
📝 Quick Revision
Quick Revision Cheat Sheet
@RestControllerAdvice: Global exception handler. Catches exceptions from all controllers.
@ExceptionHandler: Maps exception type to handler method. Most specific match wins.
Error shape: Consistent: code, message, details, timestamp, requestId.
RFC 7807: Standard problem detail format. Native in Spring Boot 3.
Order: Specific handlers first, generic Exception.class as fallback.
Data Access — Spring Data JPA
🔥 This is the most interview-heavy section
JPA/Hibernate questions dominate Spring Boot interviews. N+1 queries, lazy loading, transactions, and entity relationships are asked in almost every round. Know the pitfalls, not just the happy path.
Repository Pattern
| Interface | Methods | When to use |
|---|---|---|
| CrudRepository | save, findById, findAll, delete, count | Basic CRUD — sufficient for most cases |
| JpaRepository | CrudRepository + flush, saveAll, batch ops, Pageable | When you need pagination, sorting, or batch operations |
| PagingAndSortingRepository | CrudRepository + findAll(Pageable), findAll(Sort) | Explicit paging/sorting without full JPA features |
🔥 Query Methods — How Spring Generates SQL
Spring Data JPA parses method names and generates queries automatically. The method name is the query. This is one of the most magical (and most asked-about) features.
public interface UserRepository extends JpaRepository<User, Long> {
// Spring generates: SELECT * FROM users WHERE email = ?
Optional<User> findByEmail(String email);
// SELECT * FROM users WHERE status = ? AND age > ?
List<User> findByStatusAndAgeGreaterThan(String status, int age);
// SELECT * FROM users WHERE name LIKE '%keyword%' ORDER BY created_at DESC
List<User> findByNameContainingOrderByCreatedAtDesc(String keyword);
// SELECT COUNT(*) FROM users WHERE status = ?
long countByStatus(String status);
// SELECT * FROM users WHERE email = ? (returns boolean)
boolean existsByEmail(String email);
// Custom JPQL when method names get too long
@Query("SELECT u FROM User u WHERE u.department.name = :dept AND u.active = true")
List<User> findActiveByDepartment(@Param("dept") String department);
// Native SQL for complex queries
@Query(value = "SELECT * FROM users WHERE created_at > NOW() - INTERVAL '30 days'",
nativeQuery = true)
List<User> findRecentUsers();
}
🔥 Entity Relationships
| Relationship | Annotation | Default fetch | Example |
|---|---|---|---|
| One-to-One | @OneToOne | EAGER | User → Profile |
| Many-to-One | @ManyToOne | EAGER | Order → User |
| One-to-Many | @OneToMany | LAZY | User → Orders |
| Many-to-Many | @ManyToMany | LAZY | Student ↔ Course |
💡 Best practice
Always set fetch = FetchType.LAZY on all relationships. Eager fetching loads related entities even when you don't need them — this cascades and can load your entire database into memory. Use JOIN FETCH or @EntityGraph when you explicitly need the related data.
🔥 N+1 Query Problem — MUST KNOW
Fetch 100 orders. Each has a lazy user. Accessing order.getUser() for each fires 100 separate queries. Total: 1 (orders) + 100 (users) = 101 queries. This is the #1 performance killer in JPA applications.
// ❌ N+1 — 101 queries for 100 orders
List<Order> orders = orderRepo.findAll();
orders.forEach(o -> log.info(o.getUser().getName())); // 100 extra SELECTs!
// ✅ Fix 1: JOIN FETCH in JPQL — single query
@Query("SELECT o FROM Order o JOIN FETCH o.user")
List<Order> findAllWithUsers();
// ✅ Fix 2: @EntityGraph — declarative
@EntityGraph(attributePaths = {"user", "items"})
List<Order> findAll();
// ✅ Fix 3: @BatchSize — Hibernate batches lazy loads
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
@BatchSize(size = 50) // loads users in batches of 50
private User user;
}
Pagination & Sorting
// Repository — just add Pageable parameter
Page<User> findByStatus(String status, Pageable pageable);
// Service
public Page<UserDto> getActiveUsers(int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
return userRepo.findByStatus("ACTIVE", pageable)
.map(this::toDto); // map entity to DTO
}
// Controller
@GetMapping
public Page<UserDto> listUsers(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return userService.getActiveUsers(page, size);
}
// Response includes: content, totalElements, totalPages, number, size
🔥 @Transactional Deep Dive
@Transactional wraps the method in a database transaction using a proxy. Three critical gotchas that interviewers love:
- Public methods only — the proxy can't intercept private/protected methods
- Self-invocation bypasses proxy — calling another @Transactional method from the same class goes through
this, not the proxy - Checked exceptions don't rollback — only unchecked (RuntimeException) trigger rollback by default. Use
rollbackFor = Exception.class
| Propagation | Behavior | Use case |
|---|---|---|
| REQUIRED (default) | Join existing tx or create new | Most service methods |
| REQUIRES_NEW | Suspend current, create new tx | Audit logs that must persist even if parent fails |
| NESTED | Savepoint within current tx | Partial rollback within a larger operation |
| SUPPORTS | Use tx if exists, else run without | Read-only methods that work either way |
| NOT_SUPPORTED | Suspend current tx, run without | Operations that shouldn't be in a tx |
@Service
public class OrderService {
// ✅ Correct — public method, proxy intercepts
@Transactional
public void placeOrder(OrderRequest request) {
Order order = orderRepo.save(toEntity(request));
paymentService.charge(order); // if this fails, order save is rolled back
}
// ❌ Self-invocation — proxy is bypassed!
public void processOrders(List<OrderRequest> requests) {
requests.forEach(this::placeOrder); // calls through 'this', not proxy
}
// ✅ Fix: inject self or use TransactionTemplate
@Autowired private OrderService self; // proxy reference
public void processOrders(List<OrderRequest> requests) {
requests.forEach(self::placeOrder); // calls through proxy
}
// ✅ Rollback on checked exceptions
@Transactional(rollbackFor = Exception.class)
public void riskyOperation() throws PaymentException {
// checked exception will now trigger rollback
}
}
📝 Quick Revision
Quick Revision Cheat Sheet
JpaRepository: Extends CrudRepository. Adds pagination, batch ops, flush.
Query methods: findByEmailAndStatus → Spring generates SQL from method name.
N+1: Lazy collections cause N extra queries. Fix: JOIN FETCH or @EntityGraph.
Fetch types: Always use LAZY. Use JOIN FETCH when you need related data.
@Transactional: Proxy-based. Public only. Self-invocation bypasses. Checked exceptions don't rollback.
Propagation: REQUIRED (default), REQUIRES_NEW (independent tx), NESTED (savepoint).
Security — Spring Security
Spring Security is a filter-based framework that handles authentication (who are you?) and authorization (what can you do?). It's powerful but complex — understanding the filter chain architecture is the key to mastering it.
🔥 Filter Chain Architecture
Every request passes through a chain of security filters before reaching your controller. Each filter handles one concern. The order matters — authentication must happen before authorization.
- CorsFilter — handles CORS preflight
- CsrfFilter — CSRF protection (disabled for stateless APIs)
- Your JWT Filter — extract and validate token
- UsernamePasswordAuthenticationFilter — form login (if enabled)
- AuthorizationFilter — checks roles/permissions
- ExceptionTranslationFilter — converts security exceptions to HTTP responses
🔥 SecurityFilterChain Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // stateless API — no CSRF needed
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // public endpoints
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
.anyRequest().authenticated() // everything else needs auth
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(ex -> ex
.authenticationEntryPoint((req, res, e) ->
res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"))
.accessDeniedHandler((req, res, e) ->
res.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden"))
)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
🔥 JWT Authentication Flow
The JWT filter runs before Spring's authentication filter. It extracts the token from the Authorization header, validates it (signature, expiry), and sets the SecurityContext with the user's details. Downstream filters and controllers can then access the authenticated user.
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
chain.doFilter(request, response); // no token — let other filters handle
return;
}
String token = header.substring(7);
try {
Claims claims = jwtService.validateToken(token);
var auth = new UsernamePasswordAuthenticationToken(
claims.getSubject(),
null,
List.of(new SimpleGrantedAuthority("ROLE_" + claims.get("role")))
);
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (JwtException e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token");
return;
}
chain.doFilter(request, response);
}
}
Method-Level Security
@Configuration
@EnableMethodSecurity // enables @PreAuthorize, @PostAuthorize
public class MethodSecurityConfig {}
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) { ... }
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
public UserDto getUser(Long userId) { ... }
@PostAuthorize("returnObject.owner == authentication.principal.username")
public Document getDocument(Long docId) { ... }
}
CORS Configuration
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://myapp.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
📝 Quick Revision
Quick Revision Cheat Sheet
Filter chain: CORS → CSRF → JWT filter → Auth filter → Authorization → Exception handling.
JWT flow: Extract token → validate → set SecurityContext → chain continues.
SecurityFilterChain: Bean that configures which endpoints need auth and how.
@PreAuthorize: Method-level security. SpEL expressions for role/permission checks.
CORS: Configure allowed origins, methods, headers. CorsConfigurationSource bean.
Testing
Spring Boot provides a layered testing approach — test slices that load only the parts of the application you need. Understanding which annotation to use and when is a common interview topic.
🔥 Test Slices — The Core Concept
| Annotation | What it loads | Use case | Speed |
|---|---|---|---|
| @SpringBootTest | Full application context | Integration tests, end-to-end flows | Slowest |
| @WebMvcTest | Controllers + filters + advice only | Testing REST endpoints in isolation | Fast |
| @DataJpaTest | JPA repos + embedded DB | Testing queries and entity mappings | Fast |
| @MockBean | Replaces a bean with a Mockito mock | Isolating layers in slice tests | — |
| @JsonTest | Jackson serialization only | Testing JSON serialization/deserialization | Fastest |
🔥 Interview favorite
"When would you use @WebMvcTest vs @SpringBootTest?" — @WebMvcTest for testing controller logic in isolation (fast, no DB). @SpringBootTest for testing the full request flow including service and repository layers (slow, needs DB or mocks).
@WebMvcTest — Testing Controllers
@WebMvcTest(UserController.class) // loads only this controller
class UserControllerTest {
@Autowired private MockMvc mockMvc;
@MockBean private UserService userService; // mock the service layer
@Test
void shouldReturnUser() throws Exception {
var user = new UserDto(1L, "Alice", "alice@example.com");
when(userService.findById(1L)).thenReturn(user);
mockMvc.perform(get("/api/v1/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Alice"))
.andExpect(jsonPath("$.email").value("alice@example.com"));
}
@Test
void shouldReturn400ForInvalidInput() throws Exception {
var invalidBody = "{\"name\": \"\", \"email\": \"not-an-email\"}";
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(invalidBody))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
}
@Test
void shouldReturn404WhenNotFound() throws Exception {
when(userService.findById(999L))
.thenThrow(new ResourceNotFoundException("User not found"));
mockMvc.perform(get("/api/v1/users/999"))
.andExpect(status().isNotFound());
}
}
@DataJpaTest — Testing Repositories
@DataJpaTest // loads JPA components + embedded H2 by default
class UserRepositoryTest {
@Autowired private UserRepository userRepo;
@Autowired private TestEntityManager em;
@Test
void shouldFindByEmail() {
em.persistAndFlush(new User("Alice", "alice@example.com"));
Optional<User> found = userRepo.findByEmail("alice@example.com");
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("Alice");
}
@Test
void shouldReturnEmptyForNonExistentEmail() {
Optional<User> found = userRepo.findByEmail("nobody@example.com");
assertThat(found).isEmpty();
}
}
@MockBean vs @Mock
| Annotation | Context | When to use |
|---|---|---|
| @MockBean | Spring context — replaces bean in container | Slice tests (@WebMvcTest, @SpringBootTest) |
| @Mock (Mockito) | No Spring context — plain unit test | Service/logic tests without Spring |
Testcontainers — Real DB in Tests
Embedded H2 doesn't match production Postgres behavior. Testcontainers spins up a real Postgres (or any DB) in Docker for your tests. Slower than H2 but catches real SQL compatibility issues. Use for integration tests, not unit tests.
@SpringBootTest
@Testcontainers
class OrderIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
.withDatabaseName("testdb");
@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);
}
@Test
void shouldPlaceOrderEndToEnd() {
// full integration test with real Postgres
}
}
Testing Strategy
| Layer | Test type | Annotation | What to test |
|---|---|---|---|
| Controller | Slice test | @WebMvcTest | Request mapping, validation, response shape, status codes |
| Service | Unit test | @Mock (Mockito) | Business logic, edge cases, error handling |
| Repository | Slice test | @DataJpaTest | Custom queries, entity mappings, constraints |
| Full flow | Integration | @SpringBootTest + Testcontainers | End-to-end happy path, critical flows |
📝 Quick Revision
Quick Revision Cheat Sheet
@WebMvcTest: Controller slice. MockMvc + @MockBean for services. Fast.
@DataJpaTest: Repository slice. Embedded DB. Tests queries and mappings.
@SpringBootTest: Full context. Use for integration tests. Slowest.
@MockBean: Replaces bean in Spring context. @Mock for plain Mockito tests.
Testcontainers: Real DB in Docker. Use for integration tests that need Postgres behavior.
Caching, Async & Scheduling
Spring Boot provides declarative abstractions for caching, async execution, and scheduling. A single annotation can add caching to a method or make it run asynchronously. Understanding the internals and pitfalls is what interviewers test.
🔥 @Cacheable / @CacheEvict
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
// Simple in-memory cache (use Redis in production)
return new ConcurrentMapCacheManager("users", "products");
}
}
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public UserDto findById(Long id) {
log.info("Cache miss — querying DB for user {}", id);
return userRepo.findById(id).map(this::toDto)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
}
@CacheEvict(value = "users", key = "#id")
public UserDto updateUser(Long id, UpdateUserRequest request) {
// cache entry for this id is evicted after method executes
return userRepo.findById(id).map(user -> {
user.setName(request.name());
return toDto(userRepo.save(user));
}).orElseThrow();
}
@CacheEvict(value = "users", allEntries = true)
public void clearUserCache() {
// evicts all entries in the "users" cache
}
}
| Annotation | Behavior | Use case |
|---|---|---|
| @Cacheable | Returns cached value if exists, else executes method and caches result | Read-heavy methods (findById, getConfig) |
| @CacheEvict | Removes entry from cache | After updates or deletes |
| @CachePut | Always executes method and updates cache | When you want to refresh cache on every call |
💡 Cache providers
ConcurrentMapCacheManager is fine for dev. In production, use Redis (spring-boot-starter-data-redis) or Caffeine (spring-boot-starter-cache + Caffeine dependency) for TTL support, size limits, and distributed caching.
🔥 @Async — Asynchronous Execution
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
@Service
public class NotificationService {
@Async // runs on a separate thread from the pool
public CompletableFuture<Void> sendWelcomeEmail(String email) {
emailClient.send(email, "Welcome!", "...");
return CompletableFuture.completedFuture(null);
}
@Async
public CompletableFuture<ReportDto> generateReport(Long userId) {
// heavy computation — doesn't block the caller
ReportDto report = buildReport(userId);
return CompletableFuture.completedFuture(report);
}
}
🔥 @Async gotchas
Same proxy rules as @Transactional: (1) only works on public methods, (2) self-invocation bypasses the proxy — calling an @Async method from the same class runs it synchronously. Always configure a custom thread pool — the default uses SimpleAsyncTaskExecutor which creates a new thread per task (no pooling).
@Scheduled — Cron Jobs
@Configuration
@EnableScheduling
public class SchedulingConfig {}
@Component
public class ScheduledTasks {
@Scheduled(fixedRate = 60000) // every 60 seconds
public void refreshCache() {
cacheService.refreshHotData();
}
@Scheduled(cron = "0 0 2 * * ?") // every day at 2 AM
public void cleanupExpiredSessions() {
sessionRepo.deleteExpired();
}
@Scheduled(fixedDelay = 30000) // 30s after previous execution completes
public void processQueue() {
queueService.processPendingItems();
}
}
| Property | Behavior | Example |
|---|---|---|
| fixedRate | Runs every N ms regardless of previous execution | Heartbeat checks, cache refresh |
| fixedDelay | Waits N ms after previous execution completes | Queue processing (avoid overlap) |
| cron | Cron expression (second min hour day month weekday) | Nightly cleanup, weekly reports |
📝 Quick Revision
Quick Revision Cheat Sheet
@Cacheable: Cache method result. Key by params. Use Redis/Caffeine in prod.
@CacheEvict: Remove from cache on update/delete. allEntries=true to clear all.
@Async: Runs on separate thread. Configure thread pool. Proxy rules apply.
@Scheduled: fixedRate (every N ms), fixedDelay (after completion), cron (expression).
Proxy gotcha: Self-invocation bypasses proxy for @Cacheable, @Async, @Transactional.
Microservices Patterns
Spring Cloud extends Spring Boot for distributed systems. Each microservice is a standalone Boot app with its own database, deployed independently. The challenge is coordination — discovery, communication, resilience, and configuration.
🔥 Service Discovery
In a microservices architecture, services need to find each other. Hardcoding URLs breaks when services scale or move. Service discovery solves this: services register themselves with a registry (Eureka, Consul), and clients look up the registry to find available instances.
| Tool | Type | How it works |
|---|---|---|
| Eureka (Netflix) | Client-side discovery | Services register with Eureka server. Clients query Eureka to find instances. |
| Consul (HashiCorp) | Server-side discovery | Services register. Consul provides DNS/HTTP API for lookup. |
| Kubernetes DNS | Platform-native | K8s provides built-in service discovery via DNS names. |
API Gateway
A single entry point for all client requests. Routes requests to the right microservice, handles cross-cutting concerns (auth, rate limiting, logging), and can aggregate responses from multiple services. Spring Cloud Gateway is the standard choice in the Spring ecosystem.
// Spring Cloud Gateway configuration
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r
.path("/api/users/**")
.filters(f -> f
.stripPrefix(1)
.addRequestHeader("X-Request-Source", "gateway"))
.uri("lb://user-service")) // lb:// = load-balanced via discovery
.route("order-service", r -> r
.path("/api/orders/**")
.uri("lb://order-service"))
.build();
}
}
🔥 Circuit Breaker — Resilience4j
When a downstream service is failing, continuing to call it wastes resources and cascades failures. A circuit breaker monitors failure rates and "opens" the circuit when failures exceed a threshold — subsequent calls fail fast without hitting the broken service. After a timeout, it allows a few test requests to check if the service recovered.
| State | Behavior | Transitions to |
|---|---|---|
| CLOSED (normal) | All requests pass through. Failures are counted. | OPEN when failure rate exceeds threshold |
| OPEN (tripped) | All requests fail immediately (fallback). No calls to downstream. | HALF_OPEN after wait duration |
| HALF_OPEN (testing) | Limited requests pass through to test recovery. | CLOSED if successful, OPEN if still failing |
// application.yml
// resilience4j.circuitbreaker.instances.paymentService:
// failureRateThreshold: 50 # open after 50% failures
// waitDurationInOpenState: 10s # wait 10s before half-open
// slidingWindowSize: 10 # evaluate last 10 calls
@Service
public class OrderService {
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
public PaymentResult processPayment(Order order) {
return paymentClient.charge(order.getTotal()); // calls external service
}
// Fallback when circuit is open or call fails
private PaymentResult paymentFallback(Order order, Throwable throwable) {
log.warn("Payment service unavailable, queuing for retry", throwable);
retryQueue.enqueue(order);
return PaymentResult.pending("Payment queued for retry");
}
}
Inter-Service Communication
| Style | How | Pros | Cons |
|---|---|---|---|
| Synchronous (REST) | HTTP calls between services | Simple, familiar, request-response | Tight coupling, cascading failures, latency |
| Synchronous (gRPC) | Binary protocol, protobuf | Fast, type-safe, streaming support | More complex setup, not browser-friendly |
| Asynchronous (messaging) | Kafka, RabbitMQ events | Decoupled, resilient, scalable | Eventual consistency, harder to debug |
💡 Interview answer
"I prefer async messaging for most inter-service communication. It decouples services — if the order service publishes an OrderPlaced event, the notification service and inventory service consume it independently. If one is down, messages queue up and get processed when it recovers. I use synchronous REST only when the caller needs an immediate response."
Config Server & Distributed Tracing
- Spring Cloud Config — centralized configuration for all services. Stores config in Git. Services pull config on startup and can refresh without restart.
- Distributed tracing (Micrometer + Zipkin) — traces a request across multiple services. Each service adds a trace ID to logs and headers. Zipkin visualizes the full request path and latency per service.
📝 Quick Revision
Quick Revision Cheat Sheet
Service discovery: Eureka/Consul for dynamic lookup. K8s DNS in containerized environments.
API Gateway: Single entry point. Routing, auth, rate limiting. Spring Cloud Gateway.
Circuit breaker: CLOSED → OPEN (fail fast) → HALF_OPEN (test). Resilience4j.
Communication: Async messaging preferred. REST for immediate responses. gRPC for performance.
Config server: Centralized config in Git. Services pull on startup.
Actuator, Monitoring & Production
Spring Boot Actuator exposes production-ready endpoints for health checks, metrics, and diagnostics. Combined with Micrometer and external tools, it gives you full observability into your running application.
🔥 Actuator Endpoints
| Endpoint | What it shows | Default exposure |
|---|---|---|
| /actuator/health | Application health status (UP/DOWN) | Exposed over HTTP |
| /actuator/info | Application info (version, git commit) | Exposed over HTTP |
| /actuator/metrics | Application metrics (JVM, HTTP, custom) | Exposed over JMX only |
| /actuator/env | Environment properties (with sanitization) | Not exposed by default |
| /actuator/beans | All beans in the ApplicationContext | Not exposed by default |
| /actuator/loggers | View and change log levels at runtime | Not exposed by default |
# Expose specific actuator endpoints
management:
endpoints:
web:
exposure:
include: health, info, metrics, prometheus
endpoint:
health:
show-details: when-authorized # show DB, disk, Redis status
health:
db:
enabled: true
redis:
enabled: true
Custom Health Indicators
@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {
@Override
public Health health() {
try {
boolean reachable = paymentClient.ping();
if (reachable) {
return Health.up()
.withDetail("provider", "Stripe")
.withDetail("latency", "45ms")
.build();
}
return Health.down()
.withDetail("error", "Payment gateway unreachable")
.build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
// GET /actuator/health → { "status": "UP", "components": { "paymentGateway": { ... } } }
Micrometer Metrics + Prometheus
Micrometer is the metrics facade for Spring Boot (like SLF4J for logging). It collects metrics and exports them to monitoring systems. Prometheus scrapes the /actuator/prometheus endpoint, and Grafana visualizes the data. Spring Boot auto-configures JVM metrics, HTTP request metrics, and database connection pool metrics.
@Service
public class OrderService {
private final Counter orderCounter;
private final Timer orderTimer;
public OrderService(MeterRegistry registry) {
this.orderCounter = Counter.builder("orders.placed")
.description("Total orders placed")
.tag("type", "online")
.register(registry);
this.orderTimer = Timer.builder("orders.processing.time")
.description("Order processing duration")
.register(registry);
}
public OrderDto placeOrder(OrderRequest request) {
return orderTimer.record(() -> {
OrderDto order = processOrder(request);
orderCounter.increment();
return order;
});
}
}
Structured Logging
Use MDC (Mapped Diagnostic Context) to add request-scoped data (request ID, user ID) to every log line. This makes debugging production issues across distributed systems possible. Spring Boot uses Logback by default — configure structured JSON logging for production.
@Component
public class RequestIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String requestId = Optional.ofNullable(request.getHeader("X-Request-Id"))
.orElse(UUID.randomUUID().toString());
MDC.put("requestId", requestId);
response.setHeader("X-Request-Id", requestId);
try {
chain.doFilter(request, response);
} finally {
MDC.clear(); // prevent memory leaks
}
}
}
// Every log line now includes: {"requestId": "abc-123", "message": "Order placed", ...}
Graceful Shutdown
Enable with server.shutdown=graceful. When the app receives a shutdown signal, it stops accepting new requests but finishes processing in-flight ones. Set spring.lifecycle.timeout-per-shutdown-phase=30s to control how long to wait before force-killing.
📝 Quick Revision
Quick Revision Cheat Sheet
Actuator: /health, /metrics, /info. Expose selectively. Never expose /env in prod.
Health indicators: Custom checks for external deps (DB, Redis, payment gateway).
Micrometer: Metrics facade. Counter, Timer, Gauge. Export to Prometheus/Grafana.
MDC logging: Add requestId/userId to every log line. Clear in finally block.
Graceful shutdown: server.shutdown=graceful. Finish in-flight requests before stopping.
Top 30 Interview Questions
These are the questions that actually get asked
Compiled from real interview experiences at product companies. Grouped by topic. Practice answering each in 2-3 minutes.
Core & IoC (1-6)
Q:1. What is Inversion of Control and how does Spring implement it?
A: IoC means the framework controls object creation and wiring, not your code. Spring implements it through the ApplicationContext container — you declare beans (via annotations or @Bean methods), and Spring creates them, resolves dependencies, and manages their lifecycle. The key benefit: loose coupling and testability.
Q:2. Explain the difference between @Component, @Service, @Repository, and @Controller.
A: All register beans via component scanning. @Component is the base. @Service is semantic (no extra behavior). @Repository adds persistence exception translation — wraps JDBC/JPA exceptions into Spring's DataAccessException hierarchy. @Controller enables request mapping. @RestController adds @ResponseBody.
Q:3. How does Spring Boot auto-configuration work?
A: @EnableAutoConfiguration loads auto-config classes from META-INF/spring/...AutoConfiguration.imports. Each class uses @Conditional annotations to decide whether to activate — @ConditionalOnClass checks the classpath, @ConditionalOnMissingBean backs off if you define your own bean. This is why adding a starter dependency automatically configures everything.
Q:4. What is the bean lifecycle in Spring?
A: Constructor → dependency injection → @PostConstruct → InitializingBean.afterPropertiesSet() → custom init → ready to use → @PreDestroy → DisposableBean.destroy() → garbage collected. @PostConstruct is the most common hook for initialization logic.
Q:5. Constructor injection vs field injection — which is preferred and why?
A: Constructor injection. Fields can be final (immutable), dependencies are explicit in the signature, easy to test (pass mocks via constructor), and it fails fast if a dependency is missing. Field injection hides dependencies, prevents final fields, and requires reflection — making testing harder.
Q:6. What are bean scopes? When would you use prototype over singleton?
A: Singleton (default): one instance per container — use for stateless services. Prototype: new instance per injection — use for stateful objects like builders or request-specific processors. Gotcha: injecting prototype into singleton creates it once. Use ObjectProvider to get fresh instances.
JPA & Data (7-13)
Q:7. Explain the N+1 query problem in JPA and how to solve it.
A: Fetch N parent entities, each with a lazy collection. Accessing the collection triggers N separate queries (1 for parents + N for children). Fix: JOIN FETCH in JPQL loads everything in one query, @EntityGraph declaratively specifies what to fetch, @BatchSize loads children in batches instead of one-by-one.
Q:8. How does @Transactional work internally?
A: Spring creates a proxy around the bean. When a @Transactional method is called, the proxy starts a transaction before the method and commits/rolls back after. Three gotchas: only works on public methods (proxy limitation), self-invocation bypasses the proxy, and checked exceptions don't trigger rollback by default.
Q:9. Explain @Transactional propagation levels.
A: REQUIRED (default): join existing tx or create new. REQUIRES_NEW: always create a new tx, suspend current — use for audit logs that must persist even if parent fails. NESTED: savepoint within current tx for partial rollback. NOT_SUPPORTED: run without tx.
Q:10. What's the difference between JpaRepository and CrudRepository?
A: CrudRepository provides basic CRUD (save, findById, findAll, delete, count). JpaRepository extends it with JPA-specific features: flush(), saveAllAndFlush(), batch deletes, and Pageable/Sort support. Use JpaRepository when you need pagination or batch operations.
Q:11. How does Spring Data JPA generate queries from method names?
A: Spring parses the method name at startup. findByEmailAndStatus becomes SELECT * FROM users WHERE email = ? AND status = ?. It supports keywords like And, Or, Between, LessThan, Like, OrderBy, etc. For complex queries, use @Query with JPQL or native SQL.
Q:12. Lazy vs eager loading — what's the default and what are the pitfalls?
A: @OneToMany and @ManyToMany default to LAZY. @ManyToOne and @OneToOne default to EAGER. Best practice: set everything to LAZY and use JOIN FETCH when you need related data. The main pitfall: LazyInitializationException when accessing a lazy collection outside a transaction — fix with DTOs mapped inside the service layer.
Q:13. How do you handle circular dependencies?
A: With constructor injection, Spring fails fast at startup — it can't create either bean. This is a design signal. Fix: extract shared logic into a third service, use @Lazy on one dependency (creates a proxy), or use application events for decoupling. Avoid setter injection as a fix — it hides the problem.
Security & API (14-19)
Q:14. How does Spring Security's filter chain work?
A: Every request passes through a chain of servlet filters: CORS → CSRF → Authentication (your JWT filter) → Authorization → Exception handling. Each filter handles one concern. The SecurityFilterChain bean configures which endpoints need auth, which are public, and where your custom filters sit in the chain.
Q:15. How do you implement JWT authentication in Spring Security?
A: Create a filter extending OncePerRequestFilter. Extract the Bearer token from the Authorization header, validate it (signature + expiry), create an Authentication object with the user's details and roles, and set it in SecurityContextHolder. Register this filter before UsernamePasswordAuthenticationFilter in the chain.
Q:16. What is @ControllerAdvice and how do you structure global error handling?
A: @RestControllerAdvice is a global exception handler. Each @ExceptionHandler method handles a specific exception type. Structure: specific handlers first (ResourceNotFoundException → 404, ValidationException → 400, DataIntegrityViolation → 409), generic Exception.class as fallback (500). Always return a consistent error shape.
Q:17. What is the DispatcherServlet and how does request routing work?
A: DispatcherServlet is the front controller. Every request flows through: Filter chain → DispatcherServlet → HandlerMapping (finds the right controller method via @RequestMapping) → HandlerAdapter (invokes it) → Controller → HttpMessageConverter (serializes response to JSON). Interceptors run between DispatcherServlet and the controller.
Q:18. How do you handle API versioning in Spring Boot?
A: URL path versioning (/api/v1/users) is most common — explicit and easy to test. Header versioning (Accept: application/vnd.api.v2+json) is cleaner but harder to test. In Spring: use @RequestMapping with path prefix for URL versioning, or custom RequestCondition for header versioning. Pick one and be consistent.
Q:19. How do you configure connection pooling (HikariCP)?
A: HikariCP is Spring Boot's default pool. Configure via properties: spring.datasource.hikari.maximum-pool-size (default 10, set to CPU cores × 2), minimum-idle, connection-timeout, max-lifetime. Monitor with /actuator/metrics (hikaricp.connections.*). Too many connections = context switching overhead. Too few = request queuing.
Advanced (20-25)
Q:20. What is Spring AOP and where is it used internally?
A: AOP (Aspect-Oriented Programming) handles cross-cutting concerns without modifying business code. Spring uses proxy-based AOP. Internally: @Transactional (wraps methods in transactions), @Cacheable (caches results), @Async (runs on separate thread), Spring Security (method-level auth). You can create custom aspects with @Aspect + @Around.
Q:21. Explain the circuit breaker pattern with Resilience4j.
A: Monitors failure rates for downstream calls. Three states: CLOSED (normal, counting failures), OPEN (fail fast with fallback, no calls to downstream), HALF_OPEN (test with limited requests). Configure failure threshold, wait duration, and sliding window size. Use @CircuitBreaker annotation with a fallback method.
Q:22. What are Spring profiles and how do you use them?
A: Profiles swap beans and config per environment. @Profile('dev') on a bean means it's only created in dev. application-dev.yml overrides application.yml when dev profile is active. Activate with spring.profiles.active=dev (property) or -Dspring.profiles.active=prod (JVM arg). Use for different DB configs, mock services in dev, feature flags.
Q:23. How does @Async work and what are the pitfalls?
A: @Async makes a method run on a separate thread. Requires @EnableAsync. Pitfalls: (1) self-invocation bypasses proxy — runs synchronously, (2) default executor creates a new thread per task (no pooling) — always configure a ThreadPoolTaskExecutor, (3) exceptions in void methods are lost — use CompletableFuture return type to handle errors.
Q:24. What is the difference between application.properties and application.yml?
A: Same purpose, different format. Properties: flat key=value pairs, repetitive prefixes. YAML: hierarchical, cleaner for nested config, supports multi-document (--- separator) for profiles in one file. YAML is preferred for complex configs. Both support Spring Expression Language and property placeholders.
Q:25. How do you implement pagination in Spring Data JPA?
A: Add Pageable parameter to repository methods. Create PageRequest.of(page, size, Sort) in the service. Return Page<DTO> which includes content, totalElements, totalPages, number, size. In the controller, accept page/size as @RequestParam with defaults. Always map entities to DTOs before returning.
Testing & Production (26-30)
Q:26. When would you use @WebMvcTest vs @SpringBootTest?
A: @WebMvcTest loads only the web layer (controllers, filters, advice) — fast, use for testing request mapping, validation, and response shapes. @SpringBootTest loads the full context — slow, use for integration tests that need the real service and repository layers. Use @MockBean in @WebMvcTest to mock services.
Q:27. How do you test repositories with @DataJpaTest?
A: @DataJpaTest loads only JPA components with an embedded H2 database. Use TestEntityManager to set up test data. Test custom query methods, entity mappings, and constraints. For Postgres-specific behavior, use Testcontainers to spin up a real Postgres in Docker.
Q:28. What is Spring Boot Actuator and how do you use it in production?
A: Actuator exposes operational endpoints: /health (liveness/readiness), /metrics (JVM, HTTP, custom), /info (app version), /loggers (change log levels at runtime). Expose selectively — never expose /env or /beans in production. Combine with Micrometer + Prometheus + Grafana for full observability.
Q:29. How do you handle graceful shutdown?
A: Set server.shutdown=graceful in properties. On shutdown signal, Spring stops accepting new requests but finishes in-flight ones. Configure spring.lifecycle.timeout-per-shutdown-phase=30s for the maximum wait. Essential for zero-downtime deployments in Kubernetes — the pod drains connections before terminating.
Q:30. How do you structure a Spring Boot application for maintainability?
A: Layered architecture: Controller (HTTP) → Service (business logic) → Repository (data access). Each layer depends only on the layer below. Use DTOs for API input/output, entities for JPA. Never expose entities in API responses. Use @ControllerAdvice for global error handling. Keep controllers thin — delegate to services.
Last Day Revision Sheet
📋 Scan this the night before your interview
This is the compressed version of everything above. If you can explain each line, you're ready.
Core & IoC
Quick Revision Cheat Sheet
IoC: Container creates and wires objects. You don't use 'new' for managed beans.
@SpringBootApplication: @Configuration + @EnableAutoConfiguration + @ComponentScan.
Auto-config: @Conditional annotations decide what to configure based on classpath + properties.
@ConditionalOnMissingBean: Define your own bean to override auto-config. The escape hatch.
Stereotypes: @Service (logic), @Repository (data + exception translation), @Controller (web).
Bean lifecycle: Constructor → DI → @PostConstruct → ready → @PreDestroy → destroyed.
Profiles: Swap beans/config per env. application-{profile}.yml.
Dependency Injection
Quick Revision Cheat Sheet
Constructor injection: Preferred. Immutable (final), testable, fails fast. @Autowired optional with single constructor.
@Primary vs @Qualifier: @Primary = default. @Qualifier = explicit pick. Qualifier wins.
Singleton scope: Default. One per container. Use for stateless services.
Prototype gotcha: Injecting prototype into singleton creates it once. Use ObjectProvider.
Circular deps: Redesign first. @Lazy as escape hatch. Events for decoupling.
REST & Error Handling
Quick Revision Cheat Sheet
Request flow: Filter → DispatcherServlet → HandlerMapping → Controller → MessageConverter → Response.
@RestController: @Controller + @ResponseBody. Returns JSON directly.
@Valid: Triggers Bean Validation. Throws MethodArgumentNotValidException (400).
@ControllerAdvice: Global exception handler. Specific handlers first, Exception.class as fallback.
Error shape: Consistent: code, message, details, timestamp, requestId.
JPA / Hibernate
Quick Revision Cheat Sheet
N+1 problem: Lazy collections cause N extra queries. Fix: JOIN FETCH or @EntityGraph.
Fetch strategy: Always LAZY. Use JOIN FETCH when you need related data.
LazyInitException: Accessing lazy collection outside tx. Fix: map to DTOs inside service.
@Transactional: Proxy-based. Public only. Self-invocation bypasses. Checked exceptions don't rollback.
Propagation: REQUIRED (default), REQUIRES_NEW (independent tx), NESTED (savepoint).
Query methods: findByEmailAndStatus → Spring generates SQL from method name.
Security
Quick Revision Cheat Sheet
Filter chain: CORS → CSRF → JWT filter → Auth filter → Authorization → Exception handling.
JWT flow: Extract token → validate → set SecurityContext → chain continues.
@PreAuthorize: Method-level security. SpEL expressions for role/permission checks.
SecurityFilterChain: Bean that configures endpoints, auth rules, and filter order.
Testing
Quick Revision Cheat Sheet
@WebMvcTest: Controller slice. MockMvc + @MockBean. Fast.
@DataJpaTest: Repository slice. Embedded DB. Tests queries and mappings.
@SpringBootTest: Full context. Integration tests. Slowest.
Testcontainers: Real DB in Docker. Use for Postgres-specific behavior.
Caching, Async & Microservices
Quick Revision Cheat Sheet
@Cacheable: Cache method result. @CacheEvict on writes. Redis/Caffeine in prod.
@Async: Separate thread. Configure pool. Proxy rules apply. CompletableFuture for errors.
@Scheduled: fixedRate (every N ms), fixedDelay (after completion), cron (expression).
Circuit breaker: CLOSED → OPEN (fail fast) → HALF_OPEN (test). Resilience4j.
Service discovery: Eureka/Consul for dynamic lookup. K8s DNS in containers.
Production
Quick Revision Cheat Sheet
Actuator: /health, /metrics, /info. Expose selectively. Never /env in prod.
Micrometer: Counter, Timer, Gauge. Export to Prometheus. Visualize in Grafana.
MDC logging: Add requestId to every log line. Clear in finally block.
HikariCP: Default pool. max-pool-size ≈ CPU cores × 2. Monitor via actuator.
Graceful shutdown: server.shutdown=graceful. Finish in-flight requests before stopping.