Spring Boot Interview Questions¶
Spring Framework: dependency injection, Bean management, REST APIs, web development, and Spring ecosystem.
1. What are the types of dependency injection and why use constructor injection?¶
View Answer
Three Types of Dependency Injection
1. Constructor Injection ⭐ (Recommended)
Best for required dependencies.
@Service
public class UserService {
private final UserRepository repo;
// Constructor injection - all dependencies visible
public UserService(UserRepository repo) {
this.repo = repo;
}
}
Advantages:
- ✅ Immutable (final fields)
- ✅ All dependencies clear at construction
- ✅ Testable without reflection
- ✅ Fails fast if dependency missing
2. Setter Injection
For optional dependencies.
@Service
public class UserService {
private UserRepository repo;
@Autowired
public void setRepository(UserRepository repo) {
this.repo = repo;
}
}
Advantages: Allows partial initialization, dependencies can change at runtime
Disadvantages: Less clear intent, mutable
3. Field Injection ❌ (Avoid)
Discouraged in modern Spring.
@Service
public class UserService {
@Autowired
private UserRepository repo; // ❌ Hidden dependency
}
Disadvantages:
- ❌ Hidden dependencies (not obvious from constructor)
- ❌ Hard to test (need reflection or setter)
- ❌ Can cause NPE if not initialized
- ❌ Mutable
Why Constructor Injection is Best
Constructor injection makes dependencies explicit and forces initialization of all required fields. It's also the only way to truly immutable beans.
2. How does @Transactional work and what are its rollback rules?¶
View Answer
How @Transactional Works
@Transactional wraps method execution in a database transaction.
@Transactional
public void transferMoney(Account from, Account to, BigDecimal amount) {
from.debit(amount); // Within transaction
to.credit(amount); // Within transaction
}
// Commits automatically on successful return
Default Behavior
- ✅ Commits on successful return
- ❌ Rolls back on unchecked exceptions (RuntimeException, Error)
- Does NOT roll back on checked exceptions (must specify rollbackFor)
Controlling Rollback
// Roll back on ANY exception (including checked)
@Transactional(rollbackFor = Exception.class)
public void process() { ... }
// Don't roll back on specific exception
@Transactional(noRollbackFor = ValidationException.class)
public void process() { ... }
// Specify multiple exceptions
@Transactional(rollbackFor = {IOException.class, SQLException.class})
public void process() { ... }
Propagation Strategies
| Propagation | Behavior | Use Case |
|---|---|---|
| REQUIRED (default) | Use existing or create new transaction | Most operations |
| REQUIRES_NEW | Always create new, suspend current | Independent sub-transactions |
| NESTED | Create savepoint, can rollback child | Partial rollback scenarios |
| NOT_SUPPORTED | Run without transaction | Read-only operations |
| MANDATORY | Must have existing transaction (error if none) | Enforcing transaction context |
Key Point
Unchecked exceptions roll back by default; checked exceptions do not. This is often surprising and a common interview question!
3. Explain Spring bean scopes and their lifecycle¶
View Answer
Bean Scopes
| Scope | Description | Use Case | Thread-Safe |
|---|---|---|---|
| singleton | One bean per Spring container (default) | Stateless services | ❌ Must be thread-safe |
| prototype | New bean for each request | Stateful beans | ✅ Yes (one per request) |
| request | New bean per HTTP request (web only) | Request-scoped data | ✅ Yes |
| session | Bean per HTTP session (web only) | User session data | ✅ Yes |
Bean Lifecycle Hooks
@Component
public class MyBean implements InitializingBean, DisposableBean {
@PostConstruct
public void init() {
/* Called after construction & dependency injection */
}
@PreDestroy
public void cleanup() {
/* Called before bean destruction */
}
@Override
public void afterPropertiesSet() {
/* InitializingBean interface */
}
@Override
public void destroy() {
/* DisposableBean interface */
}
}
Lifecycle Order
- Constructor - Object created
- Setter methods - Dependencies injected
- @PostConstruct - Custom initialization
- afterPropertiesSet() - InitializingBean (if implemented)
- Bean is ready for use
- @PreDestroy - Custom cleanup
- destroy() - DisposableBean (if implemented)
Example: Database Connection Pool
4. What's the difference between @Component and @Bean?¶
View Answer
Quick Comparison
| Aspect | @Component | @Bean |
|---|---|---|
| Annotation Type | Class-level | Method-level |
| Where | On class definition | In @Configuration class |
| Discovery | Auto-discovered by component scanning | Explicitly defined in configuration |
| Use Case | Your own classes | Third-party libraries or complex init |
| Flexibility | Limited - class must exist | High - any object can be bean |
@Component Example
@Bean Example
@Configuration
public class AppConfig {
@Bean // Explicitly define bean
public UserService userService(UserRepository repo) {
// Complex initialization logic
UserService service = new UserService(repo);
service.configure();
return service;
}
// For third-party libraries
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
When to Use
- @Component: Your own classes, simple initialization
- @Bean: Third-party libraries, complex initialization, conditional beans
Stereotypes (Specializations of @Component)
5. How do you secure a Spring Boot REST API?¶
View Answer
Approach: Spring Security with JWT
Step 1: Authentication (Login)
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest req) {
// Validate username/password
authenticate(req.getUsername(), req.getPassword());
// Generate JWT token
String token = jwtProvider.generateToken(req.getUsername());
return ResponseEntity.ok(new JwtResponse(token));
}
Step 2: Authorization (Protected Resources)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll() // Public
.antMatchers("/api/admin/**").hasRole("ADMIN")
.antMatchers("/api/**").authenticated() // Protected
.and()
.addFilter(new JwtAuthenticationFilter());
return http.build();
}
}
Step 3: Security Layers
- Password encryption: Use BCrypt (not plain text)
- HTTPS only: Encrypt in transit
- JWT validation: Verify signature on each request
- Short expiry: Tokens expire in 15-30 minutes
- Refresh tokens: Allow generating new access tokens
- CORS: Restrict cross-origin requests
JWT Token Structure
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. // Header (algorithm)
eyJzdWIiOiIxMjM0NTY3ODkwIn0. // Payload (claims)
dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8A // Signature
Header + Payload + Signature (signed with secret)
6. What are strategies for API versioning?¶
View Answer
Strategy 1: URL Path Versioning ⭐ (Recommended)
Version in the URL path
Advantages: Explicit, search-engine friendly, easy to understand
Implementation:
@GetMapping("/api/v1/users")
public List<UserV1> getUsersV1() { ... }
@GetMapping("/api/v2/users")
public List<UserV2> getUsersV2() { ... }
Strategy 2: Request Header
Version specified in HTTP header
Advantages: Clean URL, multiple versions via same endpoint
Disadvantages: Not visible in URL, harder to test
Strategy 3: Content Negotiation
Version in Accept header with media type
Advantages: RESTful, semantic
Disadvantages: Complex, less common
Strategy 4: Query Parameter
Version as query parameter
Disadvantages: Easy to forget, caching issues
Recommendation
Use URL path versioning for clarity and simplicity. It's the most straightforward and widely understood approach.
7. When and how to use HTTP 202 Accepted for async operations?¶
View Answer
What is HTTP 202?
HTTP 202 Accepted: Request accepted for processing, but not yet complete.
Used for long-running operations that you don't want to block on.
Use Case
Client: POST /api/reports/generate (long-running job)
Server: 202 Accepted + Location header + job ID
Client: Poll GET /api/reports/{jobId} periodically
Server: 202 (still processing) or 200 (complete)
Implementation
@PostMapping("/reports/generate")
public ResponseEntity<AsyncResponse> generateReport(@RequestBody ReportRequest req) {
// Submit async task
String jobId = taskService.submitAsync(req);
// Return 202 Accepted + Location header
return ResponseEntity.accepted()
.location(URI.create("/api/reports/" + jobId))
.body(new AsyncResponse(jobId, "Processing"));
}
@GetMapping("/reports/{jobId}")
public ResponseEntity<?> checkStatus(@PathVariable String jobId) {
Job job = taskService.getJob(jobId);
if (job.isComplete()) {
return ResponseEntity.ok(job.getResult()); // 200 OK
}
if (job.isProcessing()) {
return ResponseEntity.accepted()
.body(new StatusResponse("Processing", job.getProgress())); // 202
}
if (job.isFailed()) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(job.getError()); // 409 Conflict
}
return ResponseEntity.notFound().build(); // 404
}
Complete Flow
- Client POST: Request long-running operation
- Server returns 202: Job accepted, returns Location header
- Client polls GET: Check status at Location URL
- While processing: 202 Accepted with progress
- When complete: 200 OK with result
Alternatives to Polling
- Webhooks: Server calls client when done
- WebSocket: Real-time status updates
- Server-Sent Events (SSE): One-way server push
8. What are the key differences between SOAP and REST?¶
View Answer
Comparison Table
| Aspect | SOAP | REST |
|---|---|---|
| Protocol | XML-based messaging | HTTP methods on resources |
| Message Format | XML (verbose) | JSON/XML (lightweight) |
| HTTP Methods | Usually POST for everything | GET, POST, PUT, DELETE (semantics) |
| Stateless | Can be stateful | Always stateless |
| Caching | Difficult | Built-in (HTTP caching) |
| Learning Curve | Steep (WS-* specs) | Easy (HTTP fundamentals) |
| Best For | Enterprise, financial systems | Web APIs, mobile, modern apps |
SOAP Example
POST /webservice HTTP/1.1
Content-Type: text/xml
<?xml version="1.0"?>
<soap:Envelope xmlns:soap="...">
<soap:Body>
<GetUser>
<userId>123</userId>
</GetUser>
</soap:Body>
</soap:Envelope>
REST Example
When to Choose
- Use SOAP: Legacy enterprise systems, strict contracts, WS-* security requirements
- Use REST: New APIs, web/mobile apps, microservices, public APIs
Key Difference: Method Semantics
SOAP: Everything is a method call (GetUser, CreateUser, DeleteUser)
REST: Resources with HTTP methods (GET /users/123, POST /users, DELETE /users/123)
9. What's the difference between HTTP PUT and PATCH?¶
View Answer
Quick Comparison
| Aspect | PUT | PATCH |
|---|---|---|
| Semantics | Replace entire resource | Partial update |
| Idempotent | ✅ Yes (same = same state) | ✅ Yes (usually) |
| Body | Complete resource | Only changed fields |
| Missing fields | ❌ Set to null (dangerous) | ✅ Unchanged |
| Use Case | Full replacements | Updates (most common) |
Example Scenarios
Current user: {"id": 1, "name": "John", "email": "john@old.com", "role": "USER"}
PUT /users/1
{
"name": "John",
"email": "john@new.com",
"role": "USER"
}
→ Replaces all fields. If "role" is missing, set to null (likely error!)
PATCH /users/1
{
"email": "john@new.com"
}
→ Updates only email. Name and role unchanged.
Implementation
@RestController
@RequestMapping("/api/users")
public class UserController {
// PUT: Replace entire resource
@PutMapping("/{id}")
public ResponseEntity<User> updateUserFull(@PathVariable Long id, @RequestBody User user) {
// Validate: all required fields must be present
if (user.getName() == null || user.getEmail() == null) {
return ResponseEntity.badRequest().build();
}
// Replace entire user
User updated = userService.replaceUser(id, user);
return ResponseEntity.ok(updated);
}
// PATCH: Partial update
@PatchMapping("/{id}")
public ResponseEntity<User> updateUserPartial(@PathVariable Long id, @RequestBody Map<String, Object> updates) {
// Only update provided fields
User updated = userService.partialUpdate(id, updates);
return ResponseEntity.ok(updated);
}
}
Best Practice
Use PATCH for most updates. PUT is rarely used because replacing entire resources is risky—clients often forget to include all fields, causing accidental data loss.
10. Explain Hibernate's persist(), save(), and merge() lifecycle¶
View Answer
Entity State Transitions
Core Differences
| Method | Input State | Output State | DB Effect | Use Case |
|---|---|---|---|---|
| persist() | Transient | Persistent | INSERT scheduled | New entities only |
| save() | Transient/Detached | Persistent | INSERT or UPDATE | General save (Spring Data) |
| merge() | Detached | Persistent (new instance) | UPDATE (if exists) | Re-attach modified detached entity |
persist() — New Entity, Guaranteed INSERT
User user = new User("John", "john@example.com"); // Transient
em.persist(user); // Now Persistent; INSERT queued
// ID is generated at flush/commit
System.out.println(user.getId()); // ✅ Available after commit
save() — Auto-Detect INSERT vs UPDATE
User user = new User("Jane", "jane@example.com");
User saved = userRepository.save(user);
// Spring Data's save():
// - ID null → persist() → INSERT
// - ID exists → merge() → UPDATE
merge() — Re-Attach Detached Entity (Critical!)
// Detached entity (loaded in previous session)
User user = em.find(User.class, 1L); // Persistent
em.detach(user); // Now DETACHED
user.setName("Updated Name"); // ❌ Changes NOT tracked!
// Re-attach
User managed = em.merge(user); // Returns NEW managed instance
// user != managed (different objects!)
// managed.setEmail("new@email.com"); // ✅ Changes tracked
Key Insight: Dirty Checking
@Transactional
public void updateUser(Long id, String newName) {
User user = userRepository.findById(id).get(); // Persistent
user.setName(newName);
// ❌ DON'T need to call save()!
// ✅ Dirty checking at transaction commit:
// 1. Hibernate compares current state to snapshot
// 2. Detects changes
// 3. Auto-generates UPDATE SQL
// No @Transactional = no dirty check = changes lost!
}
Common Mistake: Forgetting @Transactional
// ❌ WRONG: No @Transactional
public void updateUser(Long id, String name) {
User user = userRepository.findById(id).get();
user.setName(name); // Changes silently lost!
}
// ✅ RIGHT: With @Transactional
@Transactional
public void updateUser(Long id, String name) {
User user = userRepository.findById(id).get();
user.setName(name); // Tracked automatically
}
11. When should you choose Hibernate/JPA over MyBatis and vice versa?¶
View Answer
Quick Comparison
| Aspect | JPA/Hibernate | MyBatis |
|---|---|---|
| Paradigm | ORM (Object-Relational Mapping) | SQL Mapper |
| SQL Writing | Automatic (from objects) | Manual (full SQL control) |
| Relationships | Built-in (@OneToMany, @ManyToMany) | Manual (complex joins) |
| Boilerplate | Less (auto CRUD) | More (write mappings) |
| Performance | Good (caching, lazy loading) | Very fast (pure SQL, no overhead) |
| Flexibility | Less (constrained by ORM) | High (full SQL control) |
| Learning Curve | Moderate (lazy, cascade, context) | Low (just SQL + mappings) |
| Database Agnostic | Yes (works across DBs) | Requires dialect-specific SQL |
Use Hibernate/JPA When...
- ✅ Domain-model-driven design: Building OOP-heavy applications with entities
- ✅ Standard CRUD: Simple insert/update/delete operations
- ✅ Relationships matter: Complex entity relationships (1-to-many, many-to-many)
- ✅ Rapid development: Less boilerplate, faster to prototype
- ✅ Database portability: Need to support multiple databases
- ✅ Automatic caching: Leverage first and second-level caches
- ✅ Inheritance hierarchies: Need polymorphic queries
// JPA: Minimal boilerplate, automatic relationships
@Entity
public class User {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Order> orders; // Automatic handling
}
User user = userRepository.findById(1L).get();
List<Order> orders = user.getOrders(); // Lazy-loaded automatically
Use MyBatis When...
- ✅ Complex/optimized queries: Need fine-tuned SQL for performance
- ✅ Legacy databases: Odd schemas that don't map well to entities
- ✅ Bulk operations: Mass inserts/updates where ORM overhead matters
- ✅ Stored procedures: Heavy reliance on DB-specific features
- ✅ Team SQL expertise: Team comfortable writing/optimizing SQL
- ✅ Predictable performance: Need exact control over DB access
- ✅ Multiple data sources: Querying multiple databases with dialect-specific SQL
// MyBatis: Full SQL control, explicit mappings
@Mapper
public interface OrderMapper {
@Select("SELECT o.* FROM orders o " +
"JOIN users u ON o.user_id = u.id " +
"WHERE u.id = #{userId} AND o.amount > #{minAmount}")
List<Order> findHighValueOrdersByUser(@Param("userId") Long userId,
@Param("minAmount") BigDecimal amount);
}
List<Order> orders = orderMapper.findHighValueOrdersByUser(1L, new BigDecimal("100.00"));
Real-World Scenarios
| Scenario | Choose | |----------|--------| | Building microservice with standard CRUD endpoints | Hibernate | | E-commerce app with complex product filtering & reporting | MyBatis | | Data warehouse or analytics pipeline with bulk loads | MyBatis | | SaaS application with multi-tenant design | Hibernate | | High-frequency trading system with performance-critical queries | MyBatis | | Traditional 3-tier app with OOP domain model | Hibernate |Hybrid Approach
// Use both: Hibernate for standard operations, MyBatis for complex queries
@Service
public class OrderService {
private OrderRepository orderRepository; // Hibernate
private OrderQueryMapper orderQueryMapper; // MyBatis
public Order getSimpleOrder(Long id) {
// Simple: Use Hibernate
return orderRepository.findById(id).orElse(null);
}
public List<OrderAnalytics> getComplexAnalytics() {
// Complex: Use MyBatis for optimized SQL
return orderQueryMapper.analyzeOrderTrends();
}
}
Additional Topics¶
See Sample Topics for more areas to explore: - What's the difference between @Component, @Service, @Repository? - How does Spring Data JPA repository derivation work? - Explain the N+1 query problem and how to solve it - Explain Spring Security authentication flow - What's AOP and how does Spring implement it? - How do you test Spring applications? - What are Spring profiles and when to use them? - Explain Spring Cloud circuit breaker patterns
See also: Spring Boot Fundamentals, Spring Annotations, Interview Questions Overview