Ôn tập phỏng vấn Java Spring Boot: những điểm phải nắm chắc

Bài viết này là một checklist ôn tập có chiều sâu: mỗi phần gồm những khái niệm cốt lõi, các “bẫy” interviewer hay hỏi, và câu hỏi mẫu kèm ý chính để trả lời. Đọc phần TL;DR để biết mình đang hổng ở đâu, sau đó đi sâu vào từng phần.


TL;DR — Bản đồ kiến thức

Một buổi phỏng vấn Java Spring Boot backend thường xoay quanh 8 mảng sau. Mức độ ưu tiên dựa trên tần suất xuất hiện thực tế:

# Mảng kiến thức Ưu tiên Phải trả lời được
1 Core Java ⭐⭐⭐ OOP, equals/hashCode, Collections (đặc biệt HashMap), Stream API, Exception
2 Concurrency & JVM ⭐⭐ Thread safety, synchronized vs Lock, volatile, CompletableFuture, heap/stack, GC
3 Spring Core ⭐⭐⭐ IoC, DI, Bean scope, Bean lifecycle, AOP và proxy
4 Spring Boot ⭐⭐⭐ Auto-configuration hoạt động thế nào, starter, profile, externalized config
5 Spring MVC / REST ⭐⭐⭐ Luồng request qua DispatcherServlet, validation, exception handling, filter vs interceptor
6 Spring Data JPA & Transaction ⭐⭐⭐ Persistence context, lazy loading, N+1, @Transactional (propagation, rollback, self-invocation)
7 Spring Security ⭐⭐ Filter chain, authentication vs authorization, JWT, SecurityFilterChain
8 Testing & Microservices ⭐⭐ Slice test, Mockito, Testcontainers, caching, messaging, resilience

Nếu chỉ có một buổi tối để ôn, hãy tập trung vào 5 câu hỏi “gần như chắc chắn sẽ gặp”:

  1. HashMap hoạt động bên trong như thế nào?
  2. Dependency Injection là gì, tại sao nên dùng constructor injection?
  3. Spring Boot auto-configuration hoạt động ra sao?
  4. @Transactional hoạt động thế nào, khi nào nó không có tác dụng?
  5. N+1 query problem là gì và cách xử lý?
flowchart LR
    A[Core Java] --> B[Spring Core]
    B --> C[Spring Boot]
    C --> D[Web / REST]
    C --> E[Data JPA + Transaction]
    C --> F[Security]
    D & E & F --> G[Testing]
    G --> H[Microservices / System Design]

1. Core Java

1.1. OOP và các nguyên tắc thiết kế

Nắm chắc 4 tính chất: Encapsulation, Inheritance, Polymorphism, Abstraction. Quan trọng hơn là giải thích được bằng ví dụ thực tế trong code của bạn, không chỉ đọc định nghĩa.

Các điểm hay bị hỏi:

1.2. equals() và hashCode()

Contract: nếu a.equals(b) là true thì a.hashCode() == b.hashCode() bắt buộc phải đúng. Chiều ngược lại không bắt buộc (hash collision là bình thường).

Bẫy kinh điển: override equals mà quên hashCode, sau đó dùng object làm key trong HashMap hoặc phần tử trong HashSet — sẽ không tìm lại được object.

Với JPA entity, cần cẩn thận: đừng dựa hashCode vào ID tự sinh vì ID là null trước khi persist.

1.3. String

1.4. Collections Framework — phần quan trọng nhất của Core Java

flowchart TB
    C[Collection] --> L[List]
    C --> S[Set]
    C --> Q[Queue]
    L --> AL[ArrayList]
    L --> LL[LinkedList]
    S --> HS[HashSet]
    S --> LHS[LinkedHashSet]
    S --> TS[TreeSet]
    Q --> PQ[PriorityQueue]
    Q --> AD[ArrayDeque]
    M[Map] --> HM[HashMap]
    M --> LHM[LinkedHashMap]
    M --> TM[TreeMap]
    M --> CHM[ConcurrentHashMap]

HashMap internals — phải giải thích được trôi chảy:

  1. Bên trong là một mảng các bucket (Node<K,V>[] table), capacity mặc định 16.
  2. put(key, value): tính hash(key) (có XOR bit cao để phân bố đều), xác định index bằng (n - 1) & hash.
  3. Nếu bucket đã có phần tử (collision), phần tử mới được thêm vào linked list của bucket đó.
  4. Từ Java 8, khi một bucket có hơn 8 phần tử và table size ≥ 64, linked list chuyển thành red-black tree → lookup từ O(n) xuống O(log n).
  5. Khi số phần tử vượt capacity × load factor (mặc định 0.75), table resize gấp đôi và rehash.
  6. HashMap cho phép một key null, không thread-safe.

So sánh cần thuộc:

Đặc điểm Khi nào dùng
ArrayList Mảng động, get O(1), insert giữa O(n) Mặc định cho List
LinkedList Doubly linked list, get O(n) Hiếm khi thực sự tốt hơn ArrayList
HashMap Không thứ tự Mặc định cho Map
LinkedHashMap Giữ thứ tự insert (hoặc access) Cần thứ tự, làm LRU cache
TreeMap Sorted theo key, O(log n) Cần range query, sắp xếp
ConcurrentHashMap Thread-safe, lock theo bucket/CAS, không cho key/value null Map dùng chung giữa nhiều thread

Thêm: fail-fast iterator — sửa collection khi đang iterate (không qua iterator) sẽ ném ConcurrentModificationException.

1.5. Java 8+ features

1.6. Exception

flowchart TB
    T[Throwable] --> E[Exception]
    T --> ER["Error (OutOfMemoryError, StackOverflowError)"]
    E --> CE["Checked Exception (IOException, SQLException)"]
    E --> RE["RuntimeException - Unchecked (NullPointerException, IllegalArgumentException)"]

2. Concurrency và JVM

2.1. Concurrency

Các khái niệm phải nắm:

2.2. JVM

flowchart LR
    subgraph JVM Memory
        H["Heap (shared) - Young Gen: Eden, Survivor - Old Gen"]
        MS["Metaspace (class metadata)"]
        ST["Stack (mỗi thread một stack) - local variables, method frames"]
    end

3. Spring Core

3.1. IoC và Dependency Injection

Inversion of Control: thay vì object tự tạo dependency, container (ApplicationContext) tạo và “tiêm” vào. DI là cách hiện thực IoC.

Ba cách inject:

// 1. Constructor injection — KHUYẾN NGHỊ
@Service
public class OrderService {
    private final PaymentService paymentService;

    public OrderService(PaymentService paymentService) { // không cần @Autowired nếu chỉ có 1 constructor
        this.paymentService = paymentService;
    }
}

// 2. Setter injection — dùng cho optional dependency
// 3. Field injection (@Autowired trên field) — nên tránh

Tại sao constructor injection tốt hơn: field có thể final (immutable), dependency bắt buộc rõ ràng, dễ unit test (không cần reflection), phát hiện circular dependency sớm, và constructor quá dài là tín hiệu class đang vi phạm Single Responsibility.

3.2. Bean

Bean scope:

Scope Ý nghĩa
singleton (mặc định) Một instance cho mỗi ApplicationContext
prototype Mỗi lần request bean tạo một instance mới
request Một instance cho mỗi HTTP request
session Một instance cho mỗi HTTP session
application Một instance cho mỗi ServletContext

Bẫy hay hỏi: inject prototype bean vào singleton bean → prototype chỉ được tạo một lần lúc singleton khởi tạo. Cách xử lý: ObjectProvider<T>, @Lookup, hoặc scoped proxy.

Bẫy thứ hai: singleton bean không tự động thread-safe. Đừng lưu state có thể thay đổi (mutable state) theo request trong field của singleton.

3.3. Bean lifecycle

flowchart TB
    A[Instantiate - gọi constructor] --> B[Populate properties - inject dependency]
    B --> C["Aware callbacks (BeanNameAware, ApplicationContextAware)"]
    C --> D["BeanPostProcessor.postProcessBeforeInitialization"]
    D --> E["@PostConstruct → afterPropertiesSet() → init-method"]
    E --> F["BeanPostProcessor.postProcessAfterInitialization (tạo AOP proxy ở đây)"]
    F --> G[Bean sẵn sàng sử dụng]
    G --> H["@PreDestroy → destroy() → destroy-method"]

Ý quan trọng: AOP proxy được tạo ở bước postProcessAfterInitialization. Đây là lý do @Transactional, @Async, @Cacheable hoạt động được — và cũng là gốc rễ của bẫy self-invocation.

3.4. Circular dependency

A cần B, B cần A. Với constructor injection, Spring không thể giải quyết và báo lỗi khi startup. Từ Spring Boot 2.6, circular reference bị cấm mặc định kể cả với field/setter injection (spring.main.allow-circular-references=false). Cách xử lý đúng là thiết kế lại (tách phần dùng chung ra class thứ ba, dùng event), @Lazy chỉ là giải pháp tạm.

3.5. AOP


4. Spring Boot

4.1. Spring Boot giải quyết vấn đề gì?

So với Spring Framework thuần: auto-configuration, starter dependency, embedded server (Tomcat/Jetty/Undertow), externalized configuration, production-ready features (Actuator). Phương châm: convention over configuration, nhưng luôn có thể override.

4.2. Auto-configuration — câu hỏi gần như chắc chắn gặp

@SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan.

flowchart TB
    A["@SpringBootApplication"] --> B["@EnableAutoConfiguration"]
    B --> C["AutoConfigurationImportSelector"]
    C --> D["Đọc danh sách class từ META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"]
    D --> E{"Đánh giá các điều kiện @Conditional*"}
    E -- "Thỏa mãn" --> F[Đăng ký bean]
    E -- "Không thỏa" --> G[Bỏ qua]

Các annotation điều kiện quan trọng:

Lưu ý: file spring.factories là cơ chế cũ cho auto-configuration (Spring Boot 2.x); từ Spring Boot 3 dùng file AutoConfiguration.imports như trên.

Mẹo debug hay được hỏi: chạy với --debug hoặc debug=true để xem Condition Evaluation Report — auto-config nào được áp dụng, cái nào bị bỏ qua và vì sao. Có thể loại bỏ auto-config bằng @SpringBootApplication(exclude = ...).

4.3. Starter

Starter là một dependency “gom” các thư viện cần thiết cho một tính năng, ví dụ spring-boot-starter-web kéo theo Spring MVC, Tomcat embedded, Jackson. Nên biết cách tự viết một custom starter (autoconfigure module + starter module) nếu phỏng vấn level senior.

4.4. Configuration

4.5. Actuator

Endpoint như /actuator/health, /actuator/metrics, /actuator/info, /actuator/prometheus. Biết cách expose có chọn lọc và bảo vệ bằng Security. Liên quan: Micrometer cho metrics và tracing.

4.6. Những thay đổi lớn cần biết theo version


5. Spring MVC / REST API

5.1. Luồng xử lý request

sequenceDiagram
    participant C as Client
    participant F as Filter Chain
    participant DS as DispatcherServlet
    participant HM as HandlerMapping
    participant I as HandlerInterceptor
    participant CT as Controller
    participant MC as HttpMessageConverter

    C->>F: HTTP Request
    F->>DS: request đã qua filter (Security, CORS...)
    DS->>HM: tìm handler phù hợp
    HM-->>DS: HandlerExecutionChain
    DS->>I: preHandle()
    DS->>CT: gọi method (qua HandlerAdapter, bind argument)
    CT-->>DS: return object
    DS->>MC: serialize object sang JSON (Jackson)
    DS->>I: postHandle() rồi afterCompletion()
    DS-->>C: HTTP Response

5.2. Filter vs Interceptor

Filter HandlerInterceptor
Thuộc về Servlet API Spring MVC
Vị trí Trước DispatcherServlet Sau DispatcherServlet, quanh controller
Biết handler nào sẽ xử lý? Không Có
Use case Security, logging thô, CORS, encoding, request ID Kiểm tra quyền theo handler, audit, đo thời gian xử lý controller

5.3. Annotation và thực hành cần nắm

5.4. Spring MVC vs WebFlux

MVC: mô hình thread-per-request, blocking. WebFlux: non-blocking, event loop, dựa trên Project Reactor (Mono, Flux). WebFlux chỉ có lợi khi toàn bộ stack non-blocking. Với Java 21, virtual threads giúp MVC blocking xử lý concurrency cao mà vẫn giữ code đơn giản — đây là câu hỏi thảo luận hay gặp gần đây.


6. Spring Data JPA và Transaction

Đây là phần dễ lộ kinh nghiệm thực tế nhất, hãy chuẩn bị ví dụ từ dự án của bạn.

6.1. JPA cơ bản

6.2. Fetch type và N+1 problem

Default: @ManyToOne, @OneToOne là EAGER; @OneToMany, @ManyToMany là LAZY. Khuyến nghị thực tế: đặt tất cả thành LAZY và fetch có chủ đích.

N+1 problem: 1 query lấy danh sách N entity, sau đó mỗi entity phát sinh thêm 1 query để lấy association khi truy cập.

sequenceDiagram
    participant App
    participant DB
    App->>DB: SELECT * FROM orders (1 query, trả về N order)
    loop Với mỗi order
        App->>DB: SELECT * FROM customers WHERE id = ? (N query)
    end
    Note over App,DB: Tổng cộng N + 1 query

Cách xử lý:

// 1. JOIN FETCH
@Query("SELECT o FROM Order o JOIN FETCH o.customer")
List<Order> findAllWithCustomer();

// 2. @EntityGraph
@EntityGraph(attributePaths = {"customer"})
List<Order> findAll();

// 3. Batch fetching: spring.jpa.properties.hibernate.default_batch_fetch_size=100
// 4. DTO projection — chỉ lấy đúng các cột cần

Lưu ý: JOIN FETCH collection kết hợp pagination sẽ khiến Hibernate phân trang trong bộ nhớ (cảnh báo HHH90003004). Cách phát hiện N+1: bật SQL log, dùng Hibernate statistics, hoặc thư viện đếm số query trong test.

LazyInitializationException: truy cập lazy association sau khi session đã đóng. Cách xử lý đúng là fetch dữ liệu cần trong transaction hoặc dùng DTO. Ngoài ra nên biết Open Session In View (spring.jpa.open-in-view, mặc định true và Spring Boot log warning) — tiện nhưng giữ connection DB suốt request, thường nên tắt.

6.3. @Transactional — trọng tâm

Cách hoạt động: Spring tạo proxy quanh bean. Khi method được gọi qua proxy: mở transaction → gọi method thật → commit nếu thành công, rollback nếu có exception phù hợp.

sequenceDiagram
    participant Caller
    participant Proxy as Proxy (TransactionInterceptor)
    participant Target as OrderService thật
    participant TM as TransactionManager

    Caller->>Proxy: placeOrder()
    Proxy->>TM: begin transaction
    Proxy->>Target: placeOrder()
    Target-->>Proxy: return / throw
    alt thành công
        Proxy->>TM: commit
    else RuntimeException
        Proxy->>TM: rollback
    end
    Proxy-->>Caller: kết quả

Khi nào @Transactional KHÔNG hoạt động — câu hỏi “bẫy” phổ biến nhất:

  1. Self-invocation: method A trong cùng class gọi method B có @Transactional → gọi qua this, không qua proxy, nên annotation trên B bị bỏ qua.
  2. Method private (và với Spring 6 trở về trước, cả method không public trong nhiều trường hợp) hoặc final.
  3. Exception là checked exception → mặc định không rollback. Cần @Transactional(rollbackFor = Exception.class).
  4. Exception bị catch và nuốt bên trong method.
  5. Class không phải Spring bean (tự new).
  6. Chạy trong thread khác (ví dụ @Async, CompletableFuture) — transaction gắn với thread qua ThreadLocal.
@Service
public class OrderService {

    public void process() {
        this.save();   // ❌ self-invocation: @Transactional bị bỏ qua
    }

    @Transactional
    public void save() { ... }
}

Cách khắc phục self-invocation: tách method sang bean khác (khuyến nghị), inject chính mình qua @Lazy, hoặc dùng TransactionTemplate theo kiểu programmatic.

Propagation — ít nhất nắm 3 loại đầu:

Propagation Hành vi
REQUIRED (mặc định) Dùng transaction hiện có, không có thì tạo mới
REQUIRES_NEW Luôn tạo transaction mới, tạm dừng transaction hiện tại. Dùng cho audit log cần lưu dù transaction chính rollback
NESTED Savepoint trong transaction hiện tại (chỉ với JDBC/DataSource transaction manager)
SUPPORTS Có thì dùng, không có thì chạy không transaction
MANDATORY Bắt buộc phải có transaction sẵn, không có thì throw
NOT_SUPPORTED / NEVER Chạy ngoài transaction / throw nếu đang có transaction

Bẫy: với REQUIRED, nếu method bên trong throw RuntimeException và bên ngoài catch lại, transaction vẫn bị đánh dấu rollback-only → khi commit sẽ gặp UnexpectedRollbackException.

Các thuộc tính khác:

6.4. Concurrency trong database

6.5. Performance cơ bản

Connection pool HikariCP (mặc định của Spring Boot), index, pagination (Pageable, Slice vs Page), batch insert (hibernate.jdbc.batch_size; lưu ý GenerationType.IDENTITY vô hiệu hóa batch insert).


7. Spring Security

7.1. Kiến trúc

flowchart LR
    R[Request] --> DFP["DelegatingFilterProxy"]
    DFP --> FCP["FilterChainProxy"]
    FCP --> SFC["SecurityFilterChain: CorsFilter → CsrfFilter → Authentication filters → ExceptionTranslationFilter → AuthorizationFilter"]
    SFC --> DS[DispatcherServlet]

7.2. Cấu hình hiện đại (Spring Security 6+)

WebSecurityConfigurerAdapter đã bị loại bỏ. Cấu hình bằng bean SecurityFilterChain với lambda DSL:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())   // chỉ hợp lý với API stateless dùng token
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
            .build();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

7.3. Những điểm hay hỏi


8. Testing

8.1. Test pyramid

flowchart TB
    E2E["E2E test - ít, chậm"] --> IT["Integration test - @SpringBootTest, Testcontainers"]
    IT --> SL["Slice test - @WebMvcTest, @DataJpaTest"]
    SL --> UT["Unit test - JUnit 5 + Mockito - nhiều, nhanh"]

9. Microservices và các chủ đề mở rộng

Mức độ sâu phụ thuộc vị trí ứng tuyển. Với mid/senior, cần thảo luận được trade-off:


10. Bộ câu hỏi tự kiểm tra

Hãy thử trả lời thành tiếng mỗi câu trong khoảng 1–2 phút. Câu nào ấp úng là chỗ cần ôn lại.

Core Java

  1. HashMap xử lý collision như thế nào? Điều gì thay đổi ở Java 8?
  2. Tại sao String là immutable?
  3. ConcurrentHashMap khác Collections.synchronizedMap() và Hashtable ra sao?
  4. volatile có đủ để làm biến đếm thread-safe không?
  5. Khác biệt giữa map và flatMap trong Stream?

Spring Core / Boot

  1. Tại sao nên dùng constructor injection?
  2. Singleton bean có thread-safe không?
  3. Inject prototype bean vào singleton bean thì chuyện gì xảy ra?
  4. Giải thích cách Spring Boot tự cấu hình DataSource khi bạn chỉ thêm dependency và vài dòng config.
  5. Làm sao override một bean do auto-configuration tạo ra?

Web / Data

  1. Mô tả đường đi của một HTTP request từ client đến controller và ngược lại.
  2. Filter khác Interceptor thế nào? Khi nào dùng cái nào?
  3. N+1 problem là gì, phát hiện và xử lý ra sao?
  4. Liệt kê các trường hợp @Transactional không có tác dụng.
  5. REQUIRED khác REQUIRES_NEW thế nào? Cho một use case thực tế của REQUIRES_NEW.
  6. Optimistic locking và pessimistic locking — khi nào chọn cái nào?

Security / Testing / Architecture

  1. Mô tả luồng xác thực JWT trong một API Spring Boot.
  2. 401 và 403 khác nhau thế nào?
  3. @WebMvcTest khác @SpringBootTest thế nào?
  4. Làm sao đảm bảo vừa lưu order vào DB vừa gửi event lên Kafka mà không bị mất nhất quán?

11. Kế hoạch ôn tập gợi ý

flowchart LR
    D1["Ngày 1-2: Core Java + Collections + Concurrency"] --> D2["Ngày 3: Spring Core + AOP"]
    D2 --> D3["Ngày 4: Spring Boot + MVC/REST"]
    D3 --> D4["Ngày 5: JPA + Transaction + Isolation level"]
    D4 --> D5["Ngày 6: Security + Testing"]
    D5 --> D6["Ngày 7: Microservices + ôn bộ câu hỏi + chuẩn bị câu chuyện dự án"]

Theo từng level, trọng tâm khác nhau:


12. Lời khuyên khi vào phòng phỏng vấn

Chúc bạn phỏng vấn thành công! 🚀

Back
0%