1. Spring Boot 3 Core & Bean Lifecycle Internals
How Spring initializes, configures, and proxies beans
At the senior level, interviewers probe your understanding of what actually happens when SpringApplication.run() is invoked.
1. Spring Boot 3 Startup Sequence
- Environment Setup: Reads system properties, OS environment variables, and parses
application.yml / application.properties. - ApplicationContext Creation: Instantiates the dependency injection container (
AnnotationConfigServletWebServerApplicationContext for MVC or reactive variant for WebFlux). - Auto-Configuration Loading: In Spring Boot 3, auto-configuration classes are discovered via
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (replacing the legacy spring.factories mechanism from Spring Boot 2.x). - Embedded Web Server Initialization: Tomcat, Jetty, or Undertow is started and binds to the configured port (e.g. 8080).
2. Spring Bean Lifecycle Phases
Understanding the exact lifecycle order is critical for troubleshooting initialization bugs:
- Instantiation: JVM allocates memory for the bean instance via constructor reflection.
- Populate Properties: Dependencies injected via field, setter, or constructor injection.
- Aware Interfaces: Spring injects container references (
BeanNameAware, BeanFactoryAware, ApplicationContextAware). - BeanPostProcessor (Before Initialization):
postProcessBeforeInitialization() executes (e.g. processing @PostConstruct annotations). - Initialization:
InitializingBean.afterPropertiesSet() runs, followed by any custom initMethod. - BeanPostProcessor (After Initialization):
postProcessAfterInitialization() executes. This is where Spring creates CGLIB or JDK dynamic proxies for annotations like @Transactional, @Async, and @Cacheable! - Ready for Use: Bean is live in the container singleton registry.
- Destruction Lifecycle: Triggered on container shutdown (
@PreDestroy, DisposableBean.destroy()).
2. The @Transactional Traps & Rollback Mechanics
Four transaction pitfalls to understand
The @Transactional annotation looks simple, but hides several subtle traps that frequently cause silent data corruption or database connection pool exhaustion in production.
Trap 1: The Self-Invocation Proxy Bypass
If method saveUser() calls @Transactional public void updateUserRecords() inside the same service class, no transaction is created.
- Root Cause: Spring wraps the service bean with a dynamic proxy. When an external caller calls
saveUser(), the proxy is invoked. But internal calls within the same class invoke this.updateUserRecords(), completely bypassing the proxy and its transaction interceptor. - Fix: Inject the service into itself via
@Lazy, extract the transactional logic to a dedicated helper service, or use programmatic transactions via TransactionTemplate.
Trap 2: Default Exception Rollback Behavior
By default, Spring's declarative transaction infrastructure rolls back for unchecked exceptions (RuntimeException) and Error, but not checked exceptions. Rollback rules can be customized. Spring rollback documentation
- Example override: If checked exceptions should trigger rollback, declare an explicit rule such as:
@Transactional(rollbackFor = Exception.class)
Trap 3: Holding Connections During Remote I/O (Connection Starvation)
Long-running external HTTP, gRPC, or file operations inside a database transaction can extend resource-holding time. With common JDBC transaction setups, a connection is held once acquired; exact acquisition timing depends on the transaction manager and data-access behavior. A slow dependency can increase connection-pool pressure, so measure the workload and pool limits.
Trap 4: Transaction Propagation Pitfalls
REQUIRED (Default): Joins an existing transaction if one exists; creates a new one if not. If a child method throws a caught exception, the transaction is marked rollback-only, causing the parent method to fail with UnexpectedRollbackException on commit.REQUIRES_NEW: Suspends the current outer transaction, opens an independent physical transaction, and acquires a second database connection from the pool. Using REQUIRES_NEW carelessly can cause deadlock in connection pools!
3. Microservices Resilience: Circuit Breakers with Resilience4j
Protecting distributed services from cascading failures
In a microservices ecosystem, if Downstream Service C experiences high latency, Upstream Service B exhausts its thread pool waiting for responses, cascading the failure to Upstream Service A and taking down the entire system.
Circuit Breaker State and Trade-offs
A circuit breaker can stop repeated calls to an unhealthy dependency and allow limited test calls after a configured wait. Library behavior, thresholds, and fallback handling depend on configuration.
Circuit Breaker State Machine
- CLOSED: Normal state. All requests flow directly to the downstream service. Resilience4j tracks call outcomes (success/failure/slow call) in a sliding window (e.g., last 100 calls).
- OPEN: If the failure rate exceeds the threshold (e.g. > 50%), the circuit transitions to OPEN. All subsequent requests fail fast immediately, executing an optional fallback method without touching the downstream service.
- HALF_OPEN: After a configured duration (
waitDurationInOpenState = 10s), the circuit enters HALF_OPEN. It allows a trial number of requests (e.g. 10 calls) to pass through. If they succeed, the circuit resets to CLOSED; if any fail, it reverts back to OPEN.
4. Distributed Transactions: Saga Pattern vs 2-Phase Commit
Maintaining data consistency across autonomous microservice databases
In microservices, each service owns its private database. You cannot use a single SQL transaction across Order Service (PostgreSQL) and Payment Service (MongoDB).
2-Phase Commit (2PC / XA) Trade-offs
Two-phase commit coordinates a prepare and commit decision across transaction participants. If the coordinator or a participant becomes unavailable, prepared work may remain in doubt until recovery, depending on the resource manager and protocol implementation. This creates availability and operational trade-offs compared with local transactions and sagas.
The Saga Pattern
A Saga is a sequence of local transactions where each service updates its internal database and publishes a domain event. If any step fails, the Saga triggers Compensating Transactions that undo previous operations in reverse order.
| Approach | Mechanics | Pros | Cons |
|---|
| Choreography | Event-driven. Services listen to Kafka events and trigger local actions. | Simple, decentralized, loose coupling. | Difficult to trace workflows; cyclic dependency risk. |
|---|
| Orchestration | A centralized coordinator (e.g. OrderSagaManager) sends command messages to services. | Clear state machine, easy monitoring, explicit rollback logic. | Additional coordinator component to maintain. |
|---|
The Transactional Outbox Pattern
How can an application avoid a direct dual write between database state and a broker message?
- Write both the business record and an outbox event into the same local database transaction.
- A change-data-capture tool or asynchronous relay publishes outbox events to the broker. Delivery, retry, ordering, and duplicate behavior depend on the relay and broker configuration.
Transactional Outbox Pattern Implementation with Spring Boot 3 & Domain Events@Service
@RequiredArgsConstructor
public class OrderProcessingService {
private final OrderRepository orderRepository;
private final OutboxEventRepository outboxRepository;
private final ObjectMapper objectMapper;
/**
* Persists the business change and outbox row in the same local DB transaction.
* A separate relay publishes the committed outbox event to the broker.
*/
@Transactional(rollbackFor = Exception.class)
public OrderResponse createOrder(CreateOrderRequest request) {
// 1. Persist business entity
Order order = Order.builder()
.customerId(request.getCustomerId())
.totalAmount(request.getAmount())
.status(OrderStatus.PENDING_PAYMENT)
.createdAt(Instant.now())
.build();
Order savedOrder = orderRepository.save(order);
// 2. Prepare domain event payload
OrderCreatedEvent event = new OrderCreatedEvent(
savedOrder.getId(),
savedOrder.getCustomerId(),
savedOrder.getTotalAmount()
);
// 3. Persist event into the outbox table in the SAME database transaction
OutboxEvent outboxRecord = OutboxEvent.builder()
.aggregateType("ORDER")
.aggregateId(savedOrder.getId().toString())
.eventType("ORDER_CREATED")
.payload(writeJson(event))
.status(OutboxStatus.PENDING)
.createdAt(Instant.now())
.build();
outboxRepository.save(outboxRecord);
// Debezium CDC or an asynchronous poller captures outbox records and streams to Kafka
return OrderResponse.from(savedOrder);
}
private String writeJson(Object event) {
try {
return objectMapper.writeValueAsString(event);
} catch (JsonProcessingException e) {
throw new IllegalStateException("Failed to serialize outbox event payload", e);
}
}
}
5. Observability, Distributed Tracing & W3C Standards
Tracing requests across 20+ microservice boundaries
When a client clicks 'Checkout', the request touches the API Gateway, Order Service, Inventory Service, Payment Service, and Notification Service. If the request times out, how do you locate the culprit?
Distributed Tracing Fundamentals
- Trace ID: A globally unique identifier generated at the ingress API Gateway that remains unchanged across the entire distributed call graph.
- Span ID: Represents a single unit of work (e.g. an HTTP request, a database query, or a message consumption) within a specific service.
- Parent Span ID: Links child spans to their calling span, forming a directed acyclic graph (DAG) of the request.
Spring Boot 3: Micrometer Tracing & OpenTelemetry
In Spring Boot 2, distributed tracing was handled by Spring Cloud Sleuth. In Spring Boot 3, Sleuth has been completely replaced by Micrometer Tracing, which natively supports the W3C TraceContext specification and OpenTelemetry (OTel) exporters:
traceparent Header: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01- Automatically propagated over HTTP via
RestClient / WebClient and over Kafka message headers.
6. Java & Spring Boot 3 Modern Enhancements
Virtual Threads, RestClient, and GraalVM Native Images
Interviewers for senior roles will assess whether you have kept your skills up-to-date with Java and Spring Boot 3.2+:
1. Virtual Threads (Project Loom)
In classic Spring Boot MVC, each HTTP request ties up one operating system (OS) platform thread. Under heavy I/O (waiting for database queries or REST APIs), 200 concurrent requests exhaust Tomcat's thread pool. In Spring Boot 3.2 with Java 21+, virtual threads can be enabled with this configuration property. Confirm the runtime requirements in the versioned Spring Boot 3.2 documentation:
spring.threads.virtual.enabled=true
- The virtual-thread option changes the request-thread strategy; confirm the exact behavior and configuration for the Spring Boot version in use.
- Virtual threads can improve resource use for workloads with many blocking operations, but do not remove CPU, connection-pool, downstream-service, or memory limits. They do not guarantee a particular concurrency level or throughput.
2. The Modern RestClient
Spring Framework 6.1 introduced RestClient, a synchronous HTTP client with a fluent API. Select a client based on the application's requirements and framework version; this is not a blanket migration requirement. Spring REST clients.
References & Further Reading
Frequently Asked Questions
Why doesn't @Transactional rollback when a checked exception is thrown?
By default, Spring rolls back for unchecked exceptions and Errors, but not checked exceptions. Configure explicit rollback rules when the application requires different behavior; verify the rules for the Spring version in use.
What is the self-invocation issue in Spring @Transactional?
Spring applies transactional behavior using dynamic AOP proxies. When a method in a service calls another @Transactional method within the same service class using this.method(), the call bypasses the Spring proxy, meaning no transaction interceptor is triggered and the target method runs without transactional guarantees.
How does the Saga pattern solve distributed transactions compared to 2PC?
Two-phase commit coordinates a commit decision across participants and can leave prepared work in doubt during failures until recovery. A Saga composes local transactions and may use compensating actions; it changes the consistency and recovery model rather than providing the same atomicity as a single database transaction.
What is the Transactional Outbox pattern and why is it needed?
A transactional outbox writes the domain event in the same local transaction as the state change, then a relay publishes it. This avoids a direct dual-write gap, but delivery guarantees, duplicate handling, and ordering depend on the relay and broker configuration.
How does Resilience4j Circuit Breaker transition between CLOSED, OPEN, and HALF_OPEN?
In CLOSED state, requests execute normally while failure rates are tracked over a sliding window. If the failure threshold (e.g. 50%) is breached, the circuit flips to OPEN, failing fast without calling the downstream service. After a cooldown duration, it enters HALF_OPEN, allowing a limited test set of requests through. If successful, it resets to CLOSED; otherwise, it returns to OPEN.
How does enabling Virtual Threads in Spring Boot 3.2 impact high-concurrency microservices?
In supported Spring Boot versions, spring.threads.virtual.enabled=true selects virtual threads for supported request handling. This can help with blocking workloads, but achievable concurrency depends on database pools, downstream services, CPU, memory, and the deployed JDK; it is not a throughput guarantee.