Skip to content

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.

📚 Learn more: Spring Boot Fundamentals


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!

📚 Learn more: Spring Boot Advanced


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

  1. Constructor - Object created
  2. Setter methods - Dependencies injected
  3. @PostConstruct - Custom initialization
  4. afterPropertiesSet() - InitializingBean (if implemented)
  5. Bean is ready for use
  6. @PreDestroy - Custom cleanup
  7. destroy() - DisposableBean (if implemented)

Example: Database Connection Pool

@Component
public class DataSourceBean {
    private DataSource ds;

    @PostConstruct
    public void init() {
        // Initialize connection pool
        ds = createConnectionPool();
    }

    @PreDestroy
    public void cleanup() {
        // Close all connections
        ds.close();
    }
}

📚 Learn more: Spring Boot


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

@Component  // Auto-discovered
public class UserService {
    // Bean automatically created by Spring
}

@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)

@Service        // Business logic
@Repository     // Data access
@Controller     // Web controllers
@RestController // REST endpoints

📚 Learn more: Spring Annotations


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)

📚 Learn more: Security & Authentication


6. What are strategies for API versioning?

View Answer

Strategy 1: URL Path Versioning ⭐ (Recommended)

Version in the URL path

/api/v1/users  →  Old version
/api/v2/users  →  New version

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

GET /api/users
Header: API-Version: 1

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

GET /api/users
Accept: application/vnd.company.v1+json

Advantages: RESTful, semantic

Disadvantages: Complex, less common

Strategy 4: Query Parameter

Version as query parameter

GET /api/users?version=1
GET /api/users?v=2

Disadvantages: Easy to forget, caching issues

Recommendation

Use URL path versioning for clarity and simplicity. It's the most straightforward and widely understood approach.

📚 Learn more: API Styles


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

  1. Client POST: Request long-running operation
  2. Server returns 202: Job accepted, returns Location header
  3. Client polls GET: Check status at Location URL
  4. While processing: 202 Accepted with progress
  5. 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

📚 Learn more: API Styles & REST Design


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

GET /api/users/123 HTTP/1.1
Accept: application/json

Response:
{
  "id": 123,
  "name": "John Doe"
}

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)

📚 Learn more: Web Fundamentals


9. What's the difference between HTTP PUT and PATCH?

View Answer

Quick Comparison

AspectPUTPATCH
SemanticsReplace entire resourcePartial update
Idempotent✅ Yes (same = same state)✅ Yes (usually)
BodyComplete resourceOnly changed fields
Missing fields❌ Set to null (dangerous)✅ Unchanged
Use CaseFull replacementsUpdates (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.

📚 Learn more: API Styles & REST Design


10. Explain Hibernate's persist(), save(), and merge() lifecycle

View Answer

Entity State Transitions

Transient → [persist/save] → Persistent → [detach] → Detached → [merge] → Persistent

Core Differences

MethodInput StateOutput StateDB EffectUse Case
persist()TransientPersistentINSERT scheduledNew entities only
save()Transient/DetachedPersistentINSERT or UPDATEGeneral save (Spring Data)
merge()DetachedPersistent (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
}

📚 Learn more: JPA & ORM


11. When should you choose Hibernate/JPA over MyBatis and vice versa?

View Answer

Quick Comparison

AspectJPA/HibernateMyBatis
ParadigmORM (Object-Relational Mapping)SQL Mapper
SQL WritingAutomatic (from objects)Manual (full SQL control)
RelationshipsBuilt-in (@OneToMany, @ManyToMany)Manual (complex joins)
BoilerplateLess (auto CRUD)More (write mappings)
PerformanceGood (caching, lazy loading)Very fast (pure SQL, no overhead)
FlexibilityLess (constrained by ORM)High (full SQL control)
Learning CurveModerate (lazy, cascade, context)Low (just SQL + mappings)
Database AgnosticYes (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();
    }
}

📚 Learn more: JPA & ORM vs MyBatis


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