Welcome to MikeDaDev's Blog – notes on Java, backend & system design.

Explore
MikeDaDev's Blog
in java

spring-boot request lifecycle

Tung Bui Thanh (TungDaDev)14 mins read

Spring boot lifecycle.webp

Có bao giờ bạn debug một lỗi memory leak hay tracing một request bị nghẽn đến khuya mà tự hỏi bên dưới mấy annotation @RestController hay @Transactional thực sự đang diễn ra chuyện gì không?

Spring Boot với triết lý "convention over configuration" giúp mình đỡ việc rất nhiều. Nhưng chính sự tiện đó cũng che mất cơ chế vận hành bên dưới. Muốn tối ưu hiệu năng hay thiết kế dịch vụ chịu tải cao thì nên bóc bớt những "phép màu" bề ngoài ra để hiểu framework thực sự chạy thế nào.

Bài này đi theo một HTTP Request trong Spring Boot: từ lúc nó vào Servlet Container, qua các tầng Filter, tới Controller, xuống Database rồi quay ngược lại mang theo Response.

# overview flow

Request đi qua hệ thống không theo đường thẳng mà theo hình chữ U xuyên qua các lớp kiến trúc, theo đúng nguyên tắc Separation of Concerns.
springboot-req-flow

Giờ đi qua từng lớp (layer) một.

# layer 1: servlet container (tomcat)

Mọi thứ bắt đầu từ web server. Khi một TCP connection được thiết lập, Tomcat giao một thread để xử lý request. Từ Java 21 (và Spring Boot 3.2 trở lên) có thể bật Virtual Threads để không còn bị giới hạn bởi thread pool truyền thống, rất hợp với các I/O-bound application.

Có điều bỏ được giới hạn thread không có nghĩa là hết giới hạn. max-connections vẫn chặn số kết nối Tomcat nhận, và thường thì nút thắt chuyển xuống chỗ khác: vài nghìn virtual thread cùng tranh nhau 20 connection trong pool database (xem layer 8) thì vẫn phải xếp hàng như thường.

server:
  port: 8080
  servlet:
    context-path: /console
  tomcat:
    threads:
      max: 200 # max worker threads (ignored if virtual threads enabled)
      min-spare: 10
    max-connections: 8192
    accept-count: 100
 
# Virtual threads → mỗi request 1 virtual thread, no pool limit
spring:
  threads:
    virtual:
      enabled: true

Tomcat nhận TCP connection → assign thread → gọi Filter chain.

# layer 2: servlet filters

Filter là khái niệm của Servlet API, không phải của riêng Spring. Nó bao quanh toàn bộ vòng đời của request. Đây là chỗ hợp để gắn mã định danh (TraceID) cho Distributed Tracing, sau này truy vết log sẽ dễ hơn. (Nếu dự án đã dùng Micrometer Tracing/OpenTelemetry thì traceId được sinh và đưa vào MDC sẵn, filter dưới đây chỉ để minh hoạ cơ chế.)

@Slf4j
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestTracingFilter extends OncePerRequestFilter {
 
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
 
        String traceId = Optional.ofNullable(request.getHeader("X-Request-Id"))
                                 .orElseGet(() -> UUID.randomUUID().toString());
 
        MDC.put("requestId", traceId); // Gắn vào Mapped Diagnostic Context
        response.setHeader("X-Request-Id", traceId);
 
        long start = System.nanoTime();
        try {
            chain.doFilter(request, response); // Chuyền tay cho Filter tiếp theo
        } finally {
            long duration = (System.nanoTime() - start) / 1_000_000;
            log.info("[traceId={}] {} {} → {} ({}ms)",
                traceId, request.getMethod(), request.getRequestURI(),
                response.getStatus(), duration);
            MDC.clear(); // Bắt buộc phải dọn dẹp để tránh rò rỉ context giữa các Thread
        }
    }
}

# filter vs interceptor

FeatureFilter (Servlet)Interceptor (Spring MVC)
LevelServlet containerDispatcherServlet
AccessRaw request/responseHandler method info
ScopeAll requests (including static)Requests qua DispatcherServlet
Spring beansBoot tự đăng ký nếu là @ComponentĐăng ký qua WebMvcConfigurer
Use caseSecurity, CORS, logging, compressionAuth check, audit, locale

# layer 3: spring security filter chain

Security Filter Chain thực chất là một chuỗi các Filter đặc biệt được Spring gắn vào Tầng 2. Nó kiểm tra danh tính (Authentication) và quyền hạn (Authorization) trước khi request chạm tới logic nghiệp vụ.

Với Stateless API hiện đại (thường dùng JWT), chuỗi filter trông đại khái như dưới. Danh sách thực tế tuỳ cấu hình, bật log DEBUG cho org.springframework.security thì lúc khởi động Spring in ra đúng chuỗi filter của app bạn.

SecurityFilterChain (ordered filters):
 1. DisableEncodeUrlFilter
 2. WebAsyncManagerIntegrationFilter
 3. SecurityContextHolderFilter
 4. HeaderWriterFilter
 5. CorsFilter
 6. CsrfFilter (disabled for stateless)
 7. LogoutFilter
 8. BearerTokenAuthenticationFilter ← JWT validation
 9. RequestCacheAwareFilter
 10. SecurityContextHolderAwareRequestFilter
 11. AnonymousAuthenticationFilter
 12. SessionManagementFilter
 13. ExceptionTranslationFilter
 14. AuthorizationFilter ← URL-based access rules

JWT validation tại BearerTokenAuthenticationFilter:

  1. Extract Authorization: Bearer <token> header
  2. Decode JWT, validate signature via JWKS endpoint
  3. Check expiry, issuer, audience
  4. Convert claims → Authentication object
  5. Store in SecurityContextHolder

# layer 4: DispatcherServlet + handler mapping

Qua được Security, request vào phần của Spring MVC: DispatcherServlet. Dựa vào HandlerMapping, nó tìm đúng Controller Method để xử lý. Nhưng trước khi Controller được gọi, request còn phải đi qua các Interceptor.

// DispatcherServlet pseudocode:
HandlerExecutionChain chain = handlerMapping.getHandler(request);
// chain = controller method + interceptors
Object handler = chain.getHandler();
 
// Run interceptors pre-handle
for (HandlerInterceptor interceptor : chain.getInterceptors()) {
   if (!interceptor.preHandle(request, response, handler)) {
       return; // short-circuit (afterCompletion vẫn chạy cho các interceptor đã preHandle thành công)
   }
}
 
// Invoke controller method
// Với @RestController/@ResponseBody, JSON được ghi ra response ngay trong bước này
ModelAndView mv = handlerAdapter.handle(request, response, handler);
 
// Run interceptors post-handle (thực tế chạy theo thứ tự NGƯỢC với preHandle)
for (HandlerInterceptor interceptor : chain.getInterceptors()) {
   interceptor.postHandle(request, response, handler, mv);
}

# HandlerInterceptor

@Component
public class AuditInterceptor implements HandlerInterceptor {
 
   @Override
   public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                            Object handler) {
       // Runs AFTER security filters, BEFORE controller
       // handler = HandlerMethod (controller method info)
       if (handler instanceof HandlerMethod hm) {
           log.info("Calling {}.{}()", hm.getBeanType().getSimpleName(), hm.getMethod().getName());
       }
       return true; // continue chain
   }
 
   @Override
   public void postHandle(HttpServletRequest request, HttpServletResponse response,
                          Object handler, ModelAndView modelAndView) {
       // After controller (only if no exception).
       // Với @RestController/ResponseEntity, response body đã được ghi xong trước khi tới đây,
       // nên không sửa header/body ở postHandle được nữa
   }
 
   @Override
   public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                               Object handler, Exception ex) {
       // Luôn chạy nếu preHandle đã trả true, kể cả khi có exception.
       // Lưu ý: ex chỉ khác null khi exception KHÔNG được @ExceptionHandler xử lý
       // Good for cleanup
   }
}
 
// Register
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
   private final AuditInterceptor auditInterceptor;
 
   @Override
   public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(auditInterceptor)
           .addPathPatterns("/api/**")
           .excludePathPatterns("/api/public/**");
   }
}

# layer 5: argument resolution & validation

Từ chuỗi JSON thô hay các path variable, Spring dùng HttpMessageConverter (thường là Jackson) và Reflection để chuyển chúng thành các DTO (Data Transfer Object). Đi kèm là @Valid (Jakarta Bean Validation) để chặn dữ liệu rác.

@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
   private final OrderService orderService;
 
   @PostMapping
   public ResponseEntity<APIResponse<OrderDTO>> createOrder(
           @Valid @RequestBody CreateOrderRequest request,  // ← Jackson deserialize + validate
           @AuthenticationPrincipal Jwt jwt,                // ← extract from SecurityContext
           @RequestHeader("X-Workspace-Id") UUID workspaceId // ← header extraction
   ) {
       // If @Valid fails → MethodArgumentNotValidException → @ExceptionHandler
       // If header type conversion fails → MethodArgumentTypeMismatchException
       // If header missing → MissingRequestHeaderException
       // If body missing or malformed JSON → HttpMessageNotReadableException
       OrderDTO result = orderService.createOrder(request, jwt.getSubject());
       return ResponseEntity.status(201).body(APIResponse.success(result));
   }
}

# validation flow

@Valid @RequestBody CreateOrderRequest
 → Jackson ObjectMapper.readValue(body, CreateOrderRequest.class)
 → Jakarta Bean Validation
   → @NotNull, @NotBlank, @Size, @Pattern...
   → Custom @ValidXxx → XxxValidator.isValid()
 → If violations → MethodArgumentNotValidException
 → Else → pass to controller method

# layer 6: controller → service (aop proxies)

@RestController
@RequiredArgsConstructor
public class OrderController {
   private final OrderService orderService; // ← injected PROXY, not real instance
 
   @PostMapping("/api/orders")
   public ResponseEntity<?> create(@Valid @RequestBody CreateOrderRequest req,
                                   @AuthenticationPrincipal Jwt jwt) {
       OrderDTO result = orderService.createOrder(req, jwt.getSubject());
       return ResponseEntity.status(201).body(APIResponse.success(result));
   }
}

# aop proxy chain

Khi gọi orderService.createOrder(), thực tế gọi qua CGLIB proxy (Spring Boot mặc định proxy theo class). Vì đi qua proxy nên một method trong cùng class tự gọi method @Transactional khác (this.otherMethod()) sẽ không có transaction mới, lời gọi đó không đi qua proxy.

Proxy.createOrder()
 → @PreAuthorize check (if present)
 → @Transactional interceptor (begin TX)
 → @LogExecutionTime aspect (if present)
 → Real OrderService.createOrder()
 → @Transactional interceptor (commit or rollback)

# layer 7: service layer

@Service
@RequiredArgsConstructor
@Slf4j
public class OrderService {
   private final OrderRepository orderRepo;
   private final ProductService productService;
   private final ApplicationEventPublisher eventPublisher;
 
   @Transactional
   public OrderDTO createOrder(CreateOrderRequest req, String userId) {
       // 1. Business validation
       Product product = productService.getById(req.getProductId());
       if (!product.isAvailable()) {
           throw new BusinessException("PRODUCT_UNAVAILABLE");
       }
 
       // 2. Create entity
       Order order = Order.builder()
           .customerId(userId)
           .productId(product.getId())
           .quantity(req.getQuantity())
           .status(OrderStatus.PENDING)
           .build();
 
       // 3. Persist: entity vào persistence context; INSERT thường chờ tới lúc flush/commit
       //    (riêng ID kiểu IDENTITY thì INSERT chạy ngay để lấy ID)
       Order saved = orderRepo.save(order);
 
       // 4. Publish event: mặc định ĐỒNG BỘ, listener chạy ngay trong thread và transaction này.
       //    Muốn chạy sau khi commit thì dùng @TransactionalEventListener, muốn async thì thêm @Async
       eventPublisher.publishEvent(new OrderCreatedEvent(this, saved));
 
       // 5. Map to DTO
       return OrderMapper.INSTANCE.toDTO(saved);
   }
   // If RuntimeException/Error → @Transactional proxy rollbacks
   //    (checked exception mặc định vẫn COMMIT, trừ khi khai báo rollbackFor)
   // If success → @Transactional proxy commits
}

# layer 8: repository / data access

public interface OrderRepository extends JpaRepository<Order, UUID> {
   // Spring Data generates implementation at runtime
}

# jpa persistence flow

JDBC connection thường đã được lấy từ HikariCP ngay lúc @Transactional bắt đầu (JpaTransactionManager mở transaction), chứ không phải đợi tới lúc save(). save() cũng chưa chắc chạy SQL ngay:

@Transactional begin
 → Get JDBC connection from HikariCP pool, setAutoCommit(false)
repo.save(order)
 → isNew(order)? → EntityManager.persist(order)
 → else          → EntityManager.merge(order)
 → Hibernate:
   → Generate ID (UUID/sequence); IDENTITY thì phải INSERT ngay để lấy ID
   → Entity enters "managed" state (persistence context = first-level cache)
   → INSERT/UPDATE còn lại nằm trong action queue, chưa chạy
 → At TX commit:
   → Flush persistence context (dirty checking)
   → Execute pending SQLs (batch nếu bật hibernate.jdbc.batch_size)
   → JDBC connection.commit()
   → Return connection to pool

# connection pool (HikariCP)

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      idle-timeout: 300000
      connection-timeout: 20000
      max-lifetime: 1200000

Nếu Pool cạn, Thread phải chờ (connection-timeout). Quá thời gian đó thì HikariCP ném SQLTransientConnectionException ("Connection is not available, request timed out after ..."), Spring bọc lại thành CannotCreateTransactionException hoặc CannotGetJdbcConnectionException, làm hỏng cả chuỗi xử lý phía trên. Vì connection bị giữ suốt transaction, method @Transactional nào gọi HTTP ra ngoài hay chạy lâu sẽ giữ connection lâu, và pool cạn nhanh hơn nhiều so với tính toán. Cấu hình HikariCP cần cân nhắc cẩn thận.

Thread requests connection
 → Pool has idle connection? → Return immediately
 → Pool at max? → Wait (up to connection-timeout)
 → Timeout exceeded? → SQLTransientConnectionException

# layer 9: exception handling

Trên đường quay về, nếu tầng nào (từ Service, DB đến Controller) ném Exception, nó sẽ đi ngược lên qua chuỗi proxy. @Transactional đánh dấu rollback. Lỗi được đẩy ra DispatcherServlet và @RestControllerAdvice bắt lại.

Lúc này Exception được gói thành một Error Response có cấu trúc chuẩn, thân thiện với client. Có một ngoại lệ hay làm người ta bối rối: exception ném ra từ Filter (kể cả Spring Security) xảy ra trước DispatcherServlet, nên @RestControllerAdvice không bắt được. Lỗi 401/403 của Security do ExceptionTranslationFilter, AuthenticationEntryPoint và AccessDeniedHandler xử lý.

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
 
   // Validation errors
   @ExceptionHandler(MethodArgumentNotValidException.class)
   public ResponseEntity<APIResponse<?>> handleValidation(MethodArgumentNotValidException ex) {
       List<String> errors = ex.getBindingResult().getFieldErrors().stream()
           .map(e -> e.getField() + ": " + e.getDefaultMessage())
           .toList();
       return ResponseEntity.badRequest()
           .body(APIResponse.error("VALIDATION_ERROR", errors));
   }
 
   // Business exceptions
   @ExceptionHandler(BusinessException.class)
   public ResponseEntity<APIResponse<?>> handleBusiness(BusinessException ex) {
       log.warn("[traceId={}] Business error: {}", RequestContext.getRequestId(), ex.getMessage());
       return ResponseEntity.badRequest()
           .body(APIResponse.error(ex.getCode(), ex.getMessage()));
   }
 
   // Not found
   @ExceptionHandler(EntityNotFoundException.class)
   public ResponseEntity<APIResponse<?>> handleNotFound(EntityNotFoundException ex) {
       return ResponseEntity.status(404)
           .body(APIResponse.error("NOT_FOUND", ex.getMessage()));
   }
 
   // Catch-all
   @ExceptionHandler(Exception.class)
   public ResponseEntity<APIResponse<?>> handleUnexpected(Exception ex) {
       log.error("[traceId={}] Unexpected error", RequestContext.getRequestId(), ex);
       return ResponseEntity.status(500)
           .body(APIResponse.error("INTERNAL_ERROR", "An unexpected error occurred"));
   }
}

# exception propagation path

Exception thrown in Service/Repository
 → Bubbles up through AOP proxies
   → @Transactional catches → rollback TX
 → Reaches DispatcherServlet
 → DispatcherServlet delegates to HandlerExceptionResolver
 → @RestControllerAdvice @ExceptionHandler matched
 → Error response serialized → returned to client
 → HandlerInterceptor.afterCompletion called (ex = null vì exception đã được xử lý)
 → Filter chain unwinds (finally blocks)

# layer 10: response serialization

Jackson lại vào việc (tại MappingJackson2HttpMessageConverter), chuyển Object (DTO hoặc Error Response) thành chuỗi JSON. Việc này diễn ra ngay trong HandlerAdapter, trước postHandle của interceptor. Từ Spring Framework 7 / Spring Boot 4, Jackson 3 là mặc định và converter tương ứng là JacksonJsonHttpMessageConverter; MappingJackson2HttpMessageConverter (Jackson 2) bị đánh dấu deprecated. Response đi ngược qua các Filter, dọn các Context Map (như MDC.clear()) rồi trở thành một HTTP Response hoàn chỉnh.

// Controller returns object → Jackson serializes to JSON
@GetMapping("/orders/{id}")
public OrderDTO getOrder(@PathVariable UUID id) {
   return orderService.getById(id); // ← Jackson calls getters, serializes
}

# HttpMessageConverter chain

Return value (OrderDTO)
 → ContentNegotiation (Accept header → application/json)
 → MappingJackson2HttpMessageConverter
   → Set Content-Type: application/json
   → ObjectMapper writes dto directly to response OutputStream
   → Custom serializers (@JsonSerialize)
   → @JsonIgnore, @JsonProperty handling
 → Response body written (and usually committed)

# jackson customization

Cẩn thận khi tự khai báo bean ObjectMapper: Spring Boot sẽ lùi lại, bỏ ObjectMapper tự cấu hình của nó, kéo theo các thuộc tính spring.jackson.* không còn tác dụng. Mấy tuỳ chọn dưới đây (JavaTimeModule, tắt WRITE_DATES_AS_TIMESTAMPS, tắt FAIL_ON_UNKNOWN_PROPERTIES) thật ra Boot đã bật sẵn. Muốn chỉnh thêm thì dùng spring.jackson.* hoặc một bean Jackson2ObjectMapperBuilderCustomizer (Boot 4 với Jackson 3: JsonMapperBuilderCustomizer) sẽ an toàn hơn. Đoạn dưới đây là cách thay hẳn:

@Configuration
public class JacksonConfig {
   @Bean
   public ObjectMapper objectMapper() {
       return JsonMapper.builder()
           .addModule(new JavaTimeModule())
           .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
           .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
           .serializationInclusion(JsonInclude.Include.NON_NULL)
           .build();
   }
}

# complete timing example

Để hình dung, đây là timeline của một "Happy Path" (con số chỉ minh hoạ thứ tự, không phải số đo thật):

[0ms]    Request arrives at Tomcat
[1ms]    Filter: RequestTracingFilter (assign traceId)
[2ms]    Filter: SecurityFilterChain (JWT validation, JWKS cache hit)
[3ms]    DispatcherServlet: resolve handler
[3ms]    Interceptor: preHandle
[4ms]    Argument resolution: deserialize JSON body
[5ms]    Bean Validation: @Valid check
[5ms]    Controller method invoked
[6ms]    AOP: @Transactional begin (get connection from pool)
[7ms]    Service: business logic
[8ms]    Repository: load product, save order
[9ms]    Execute SQL query (DB round-trip)
[12ms]   Repository: return entity
[13ms]   Service: map to DTO, publish event
[14ms]   AOP: @Transactional commit (flush INSERT, commit)
[15ms]   Controller: return ResponseEntity
[16ms]   Jackson: serialize DTO → JSON
[16ms]   Interceptor: postHandle, afterCompletion
[17ms]   Filter chain: cleanup MDC
[17ms]   Response sent to client

# key concepts

# request scope

Tomcat Thread (or Virtual Thread)
 ├── SecurityContext (Authentication object)
 ├── MDC (traceId, userId for logging)
 ├── ScopedValue (RequestContext.REQUEST_ID)
 ├── LocaleContext (i18n)
 └── Transaction (bound to thread via TransactionSynchronizationManager)

Tất cả đều thread-bound → mất khi spawn async thread (cần propagation, ví dụ TaskDecorator hoặc Micrometer Context Propagation). Riêng ScopedValue gắn theo phạm vi gọi hàm chứ không theo thread, và được truyền sang thread con tạo bằng StructuredTaskScope. ScopedValue là preview từ Java 21 và chính thức ở Java 25 (JEP 506).

# filter vs interceptor vs aop

LayerWhenUse Case
FilterBefore/after entire servletSecurity, CORS, compression, tracing
InterceptorBefore/after controllerAudit, locale, permission check
AOP (@Aspect)Around any Spring bean methodLogging, transaction, caching, retry
@ControllerAdviceOn exceptionError response mapping

# order of execution (happy path)

1. Filter.doFilter (before)
2. Security filters (authentication + authorization)
3. Interceptor.preHandle
4. Argument resolution + validation
5. Controller method
6. AOP around advice on service (before, begin TX)
7. Service method (within TX)
8. Repository → DB
9. Service returns
10. AOP around advice (after, commit TX)
11. Controller returns
12. Response serialization (@ResponseBody / ResponseEntity)
13. Interceptor.postHandle
14. Interceptor.afterCompletion
15. Filter.doFilter (after/finally)

# order of execution (exception path)

1-7.  Same as happy path until exception
8.    Exception thrown
9.    @Transactional rollback
10.   AOP after-throwing advice
11.   Exception bubbles to DispatcherServlet
12.   @ExceptionHandler resolves error response
13.   Interceptor.afterCompletion (ex = null nếu đã được @ExceptionHandler xử lý)
14.   Filter finally blocks (MDC cleanup)
15.   Error response sent

Spring Boot nhìn thì phức tạp, nhưng tách từng lớp ra thì khá có trật tự, với các Design Pattern (như Chain of Responsibility, Proxy, Observer) được dùng rất bài bản.

Hiểu Lifecycle không chỉ để code chạy được mà còn để đặt đúng thứ vào đúng chỗ: Security nằm ở Filter, Business Logic đừng tràn ra Controller, và Connection Pool thì luôn phải theo dõi sát.

Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.