# 30 Pertanyaan Wawancara Spring Boot: Panduan Lengkap untuk Developer Java > Persiapkan wawancara Spring Boot dengan 30 pertanyaan penting tentang auto-configuration, starter, Spring Data JPA, keamanan, dan testing. - Published: 2026-02-07 - Updated: 2026-04-28 - Author: SharpSkill - Tags: spring boot, java, interview, backend, java interview - Reading time: 18 min --- Spring Boot telah menjadi framework utama untuk pengembangan Java enterprise. Wawancara teknis menilai pemahaman mekanisme internal, praktik terbaik, dan ekosistem Spring. Panduan ini memuat 30 pertanyaan yang paling sering diajukan, mulai dari konsep dasar hingga topik lanjutan. > **Tips persiapan** > > Pewawancara menghargai kandidat yang memahami "alasan" di balik setiap fitur. Selain sintaks, jelaskan juga masalah apa yang dipecahkan oleh Spring Boot. ## Dasar-dasar Spring Boot ### 1. Apa perbedaan antara Spring dan Spring Boot? Spring adalah framework modular yang menyediakan dependency injection, manajemen transaksi, dan integrasi dengan banyak teknologi. Spring Boot merupakan lapisan abstraksi di atas Spring yang menyederhanakan konfigurasi serta proses startup aplikasi. ```java // Application.java // With Spring Boot: a single annotation to start @SpringBootApplication public class Application { public static void main(String[] args) { // SpringApplication configures and starts the Spring context SpringApplication.run(Application.class, args); } } ``` Spring Boot menyediakan auto-configuration, dependency starter, server tertanam, dan metrik produksi melalui Actuator. Tujuannya adalah berpindah dari ide ke kode yang berjalan dalam hitungan menit. ### 2. Bagaimana auto-configuration bekerja? Auto-configuration menganalisis classpath lalu mengonfigurasi bean yang dibutuhkan secara otomatis. Mekanisme ini bertumpu pada anotasi `@EnableAutoConfiguration` (yang termasuk dalam `@SpringBootApplication`) dan kondisi yang ditetapkan dalam kelas-kelas auto-configuration. ```java // CustomAutoConfiguration.java @Configuration @ConditionalOnClass(DataSource.class) // Activates if DataSource is on classpath @ConditionalOnMissingBean(DataSource.class) // Only acts if no DataSource exists public class CustomAutoConfiguration { @Bean @ConditionalOnProperty(name = "app.datasource.enabled", havingValue = "true") public DataSource dataSource() { // Default DataSource configuration return DataSourceBuilder.create() .driverClassName("org.h2.Driver") .url("jdbc:h2:mem:testdb") .build(); } } ``` Spring Boot memeriksa kondisi (`@ConditionalOn*`) untuk menentukan bean mana yang dibuat. Pendekatan ini menghindari konflik dan memudahkan penggantian konfigurasi default. ### 3. Apa itu starter dan bagaimana cara membuatnya? Starter adalah modul Maven/Gradle yang menggabungkan dependensi dan konfigurasi yang dibutuhkan suatu fitur. Misalnya `spring-boot-starter-web` menyertakan Spring MVC, Jackson, Tomcat tertanam, dan validasi. ```xml my-company-starter org.springframework.boot spring-boot-starter com.mycompany logging-utils ``` Untuk membuat starter kustom, definisikan kelas-kelas auto-configuration lalu daftarkan di `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`. ### 4. Jelaskan application.properties vs application.yml Kedua format memungkinkan eksternalisasi konfigurasi. YAML menawarkan sintaks yang lebih mudah dibaca untuk struktur bersarang, sementara properties tetap lebih sederhana untuk konfigurasi datar. ```properties # application.properties server.port=8080 spring.datasource.url=jdbc:postgresql://localhost:5432/mydb spring.datasource.username=admin spring.jpa.hibernate.ddl-auto=update ``` ```yaml # application.yml - clear hierarchical structure server: port: 8080 spring: datasource: url: jdbc:postgresql://localhost:5432/mydb username: admin jpa: hibernate: ddl-auto: update ``` Profile membantu menyesuaikan konfigurasi per lingkungan: `application-dev.yml`, `application-prod.yml`. Aktivasinya dilakukan melalui `spring.profiles.active`. ### 5. Bagaimana cara kerja @SpringBootApplication? Anotasi ini menggabungkan tiga anotasi utama yang mengatur perilaku aplikasi Spring Boot. ```java // DemoApplication.java // @SpringBootApplication is equivalent to these three annotations: @SpringBootConfiguration // Equivalent to @Configuration @EnableAutoConfiguration // Activates auto-configuration @ComponentScan // Scans components in package and sub-packages public class DemoApplication { public static void main(String[] args) { // Creates context, runs configurations, and starts server ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args); // The context contains all configured beans UserService userService = context.getBean(UserService.class); } } ``` Menempatkan kelas ini di akar paket sangat penting: `@ComponentScan` memindai paket tersebut beserta seluruh subpaketnya untuk menemukan komponen. ## Spring Data dan persistensi ### 6. Apa perbedaan antara JpaRepository, CrudRepository, dan PagingAndSortingRepository? Ketiga interface ini membentuk hierarki yang menawarkan kemampuan akses data secara bertingkat. ```java // UserRepository.java // CrudRepository: basic CRUD operations public interface UserCrudRepository extends CrudRepository { // save(), findById(), findAll(), deleteById(), count(), existsById() } // PagingAndSortingRepository: adds pagination and sorting public interface UserPagingRepository extends PagingAndSortingRepository { // Inherits from CrudRepository + findAll(Sort), findAll(Pageable) } // JpaRepository: complete JPA features public interface UserRepository extends JpaRepository { // Inherits from previous + flush(), saveAndFlush(), deleteInBatch() // Derived query methods List findByEmailContaining(String email); Optional findByUsernameAndActiveTrue(String username); } ``` Untuk sebagian besar kasus, `JpaRepository` direkomendasikan karena menawarkan seluruh fitur yang diperlukan dalam pengembangan aplikasi JPA. ### 7. Bagaimana mendefinisikan query khusus dengan @Query? Anotasi `@Query` memungkinkan penulisan kueri JPQL atau SQL native ketika metode turunan tidak mencukupi. ```java // OrderRepository.java public interface OrderRepository extends JpaRepository { // JPQL with named parameters @Query("SELECT o FROM Order o WHERE o.customer.id = :customerId AND o.status = :status") List findCustomerOrdersByStatus( @Param("customerId") Long customerId, @Param("status") OrderStatus status ); // Native SQL query for specific needs @Query(value = "SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '7 days'", nativeQuery = true) List findRecentOrders(); // Modifying query with @Modifying @Modifying @Transactional @Query("UPDATE Order o SET o.status = :status WHERE o.id IN :ids") int updateOrderStatuses(@Param("ids") List ids, @Param("status") OrderStatus status); } ``` Kueri JPQL bersifat portabel antar basis data, sedangkan kueri native memberikan akses ke fitur khusus DBMS. ### 8. Jelaskan manajemen transaksi dengan @Transactional Anotasi ini menetapkan batasan transaksi pada metode atau kelas. Spring menangani commit, rollback, dan propagasi secara otomatis. ```java // PaymentService.java @Service public class PaymentService { private final AccountRepository accountRepository; private final TransactionLogRepository logRepository; // Transaction with custom configuration @Transactional( propagation = Propagation.REQUIRED, // Creates or joins a transaction isolation = Isolation.READ_COMMITTED, // Isolation level timeout = 30, // Timeout in seconds rollbackFor = PaymentException.class // Rollback on this exception ) public void transferMoney(Long fromId, Long toId, BigDecimal amount) { Account from = accountRepository.findById(fromId) .orElseThrow(() -> new AccountNotFoundException(fromId)); Account to = accountRepository.findById(toId) .orElseThrow(() -> new AccountNotFoundException(toId)); from.debit(amount); // Throws exception if insufficient balance to.credit(amount); accountRepository.save(from); accountRepository.save(to); // Log will be rolled back with everything else if an exception occurs logRepository.save(new TransactionLog(fromId, toId, amount)); } } ``` Level propagasi (`REQUIRED`, `REQUIRES_NEW`, `NESTED`, dll.) menentukan bagaimana transaksi bersarang ketika metode transaksional dipanggil bertingkat. > **Jebakan umum** > > `@Transactional` hanya bekerja untuk panggilan yang melewati proxy Spring. Pemanggilan internal di dalam kelas yang sama akan melewati proxy dan mengabaikan anotasi. ### 9. Bagaimana menangani relasi Lazy vs Eager Loading? Mode pemuatan relasi memengaruhi performa secara langsung. Lazy Loading menunda pemuatan hingga diakses, Eager Loading memuat segera. ```java // User.java @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // Eager: roles are loaded with the user @ManyToMany(fetch = FetchType.EAGER) private Set roles; // Lazy: orders are only loaded on access @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) private List orders; } ``` ```java // UserService.java @Service public class UserService { @Transactional(readOnly = true) public UserDTO getUserWithOrders(Long userId) { User user = userRepository.findById(userId).orElseThrow(); // Accessing orders triggers an additional SQL query // This works because the session is open (@Transactional) List orderDTOs = user.getOrders().stream() .map(this::toDTO) .collect(Collectors.toList()); return new UserDTO(user, orderDTOs); } // Alternative: query with JOIN FETCH to avoid N+1 public UserDTO getUserWithOrdersOptimized(Long userId) { User user = userRepository.findByIdWithOrders(userId); // Orders are already loaded, no additional query return new UserDTO(user, user.getOrders()); } } ``` Masalah N+1 muncul ketika lazy loading menghasilkan satu kueri untuk tiap elemen koleksi. `JOIN FETCH` atau projection dapat menyelesaikannya. ### 10. Apa itu masalah N+1 dan bagaimana mengatasinya? Masalah N+1 terjadi ketika kueri awal (1) diikuti N kueri tambahan untuk memuat relasi tiap entitas. ```java // ArticleRepository.java public interface ArticleRepository extends JpaRepository { // PROBLEM: this query generates N+1 queries List
findAll(); // SOLUTION 1: JOIN FETCH in JPQL @Query("SELECT a FROM Article a JOIN FETCH a.author JOIN FETCH a.comments") List
findAllWithDetails(); // SOLUTION 2: Entity Graph @EntityGraph(attributePaths = {"author", "comments"}) @Query("SELECT a FROM Article a") List
findAllWithGraph(); } ``` ```java // Article.java @Entity @NamedEntityGraph( name = "Article.withDetails", attributeNodes = { @NamedAttributeNode("author"), @NamedAttributeNode("comments") } ) public class Article { @Id private Long id; @ManyToOne(fetch = FetchType.LAZY) private Author author; @OneToMany(mappedBy = "article", fetch = FetchType.LAZY) private List comments; } ``` Menggunakan `@EntityGraph` atau `JOIN FETCH` mengurangi N+1 menjadi satu kueri dengan join sehingga performa meningkat signifikan. ## REST API dan web ### 11. Bagaimana membuat REST controller dengan validasi? Spring Boot memadukan `@RestController` dengan Bean Validation untuk membangun API tangguh dengan validasi input otomatis. ```java // UserController.java @RestController @RequestMapping("/api/users") @Validated // Activates validation on method parameters public class UserController { private final UserService userService; @PostMapping @ResponseStatus(HttpStatus.CREATED) public UserResponse createUser(@Valid @RequestBody CreateUserRequest request) { // Validation is performed automatically before execution return userService.createUser(request); } @GetMapping("/{id}") public UserResponse getUser( @PathVariable @Min(1) Long id // Validation on PathVariable ) { return userService.findById(id) .orElseThrow(() -> new UserNotFoundException(id)); } @GetMapping public Page listUsers( @RequestParam(defaultValue = "0") @Min(0) int page, @RequestParam(defaultValue = "20") @Max(100) int size ) { return userService.findAll(PageRequest.of(page, size)); } } ``` ```java // CreateUserRequest.java public record CreateUserRequest( @NotBlank(message = "Name is required") @Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters") String name, @NotBlank(message = "Email is required") @Email(message = "Invalid email format") String email, @NotBlank @Pattern(regexp = "^(?=.*[A-Z])(?=.*[0-9]).{8,}$", message = "Password must contain at least 8 characters, one uppercase and one digit") String password ) {} ``` Pesan validasi dapat dipindahkan ke berkas pesan untuk keperluan internasionalisasi. ### 12. Bagaimana menangani exception secara global dengan @ControllerAdvice? Penanganan exception terpusat menyeragamkan respons error dan menghindari duplikasi kode. ```java // GlobalExceptionHandler.java @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); // Handle validation errors @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleValidationErrors(MethodArgumentNotValidException ex) { Map errors = ex.getBindingResult() .getFieldErrors() .stream() .collect(Collectors.toMap( FieldError::getField, FieldError::getDefaultMessage, (existing, replacement) -> existing )); return new ErrorResponse("VALIDATION_ERROR", "Invalid data", errors); } // Handle resource not found @ExceptionHandler(ResourceNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ErrorResponse handleNotFound(ResourceNotFoundException ex) { return new ErrorResponse("NOT_FOUND", ex.getMessage(), null); } // Handle unexpected errors @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse handleUnexpectedError(Exception ex) { log.error("Unexpected error", ex); return new ErrorResponse("INTERNAL_ERROR", "An error occurred", null); } } ``` ```java // ErrorResponse.java public record ErrorResponse( String code, String message, Map details, Instant timestamp ) { public ErrorResponse(String code, String message, Map details) { this(code, message, details, Instant.now()); } } ``` Pendekatan ini memastikan seluruh error berformat sama sehingga memudahkan pemrosesan di sisi klien. ### 13. Apa beda @RequestParam, @PathVariable, dan @RequestBody? Ketiga anotasi ini mengambil data dari bagian-bagian berbeda pada permintaan HTTP. ```java // ProductController.java @RestController @RequestMapping("/api/products") public class ProductController { // @PathVariable: extracts values from the URL // GET /api/products/123 @GetMapping("/{id}") public Product getProduct(@PathVariable Long id) { return productService.findById(id); } // @RequestParam: extracts query parameters // GET /api/products?category=electronics&minPrice=100&page=0 @GetMapping public Page searchProducts( @RequestParam(required = false) String category, @RequestParam(defaultValue = "0") BigDecimal minPrice, @RequestParam(defaultValue = "0") int page ) { return productService.search(category, minPrice, PageRequest.of(page, 20)); } // @RequestBody: deserializes the JSON request body // POST /api/products with {"name": "...", "price": ...} @PostMapping public Product createProduct(@Valid @RequestBody CreateProductRequest request) { return productService.create(request); } // Combination of all three in the same method // PUT /api/products/123?notify=true with JSON body @PutMapping("/{id}") public Product updateProduct( @PathVariable Long id, @RequestParam(defaultValue = "false") boolean notify, @Valid @RequestBody UpdateProductRequest request ) { Product updated = productService.update(id, request); if (notify) { notificationService.sendUpdateNotification(updated); } return updated; } } ``` `@PathVariable` mengidentifikasi resource tertentu, `@RequestParam` menyaring atau memaginasi hasil, dan `@RequestBody` membawa data kompleks. ### 14. Bagaimana menerapkan versioning API? Ada beberapa strategi untuk memberi versi pada REST API, masing-masing dengan kelebihan tersendiri. ```java // Version via URL (recommended for clarity) @RestController @RequestMapping("/api/v1/users") public class UserControllerV1 { @GetMapping("/{id}") public UserV1Response getUser(@PathVariable Long id) { return userService.findByIdV1(id); } } @RestController @RequestMapping("/api/v2/users") public class UserControllerV2 { @GetMapping("/{id}") public UserV2Response getUser(@PathVariable Long id) { // V2 includes additional fields return userService.findByIdV2(id); } } ``` ```java // Version via custom header @RestController @RequestMapping("/api/users") public class UserController { @GetMapping(value = "/{id}", headers = "X-API-Version=1") public UserV1Response getUserV1(@PathVariable Long id) { return userService.findByIdV1(id); } @GetMapping(value = "/{id}", headers = "X-API-Version=2") public UserV2Response getUserV2(@PathVariable Long id) { return userService.findByIdV2(id); } } ``` ```java // Version via media type (content negotiation) @RestController @RequestMapping("/api/users") public class UserMediaTypeController { @GetMapping(value = "/{id}", produces = "application/vnd.myapi.v1+json") public UserV1Response getUserV1(@PathVariable Long id) { return userService.findByIdV1(id); } @GetMapping(value = "/{id}", produces = "application/vnd.myapi.v2+json") public UserV2Response getUserV2(@PathVariable Long id) { return userService.findByIdV2(id); } } ``` Versioning lewat URL paling umum digunakan karena sederhana dan mudah terlihat di log maupun dokumentasi. ### 15. Bagaimana mengonfigurasi CORS pada Spring Boot? CORS (Cross-Origin Resource Sharing) mengatur permintaan dari origin yang berbeda. Tersedia beberapa level konfigurasi. ```java // CorsConfig.java - Global configuration @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("https://frontend.example.com", "https://admin.example.com") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .exposedHeaders("X-Total-Count", "X-Page-Count") .allowCredentials(true) .maxAge(3600); // Cache preflight for 1 hour } } ``` ```java // Per-controller or method configuration @RestController @RequestMapping("/api/public") @CrossOrigin(origins = "*", maxAge = 3600) public class PublicApiController { @GetMapping("/data") public DataResponse getPublicData() { return dataService.getPublicData(); } // Override at method level @CrossOrigin(origins = "https://specific-client.com") @PostMapping("/submit") public SubmitResponse submitData(@RequestBody SubmitRequest request) { return dataService.submit(request); } } ``` Di lingkungan produksi, batasi origin yang diperbolehkan dan gunakan HTTPS. Jangan pernah memakai `*` bersamaan dengan `allowCredentials(true)`. ## Spring Security ### 16. Bagaimana mengonfigurasi Spring Security dengan JWT? Spring Security 6+ menerapkan pendekatan fungsional pada konfigurasi keamanan. ```java // SecurityConfig.java @Configuration @EnableWebSecurity public class SecurityConfig { private final JwtAuthenticationFilter jwtFilter; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .csrf(csrf -> csrf.disable()) // Disabled for stateless API .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/public/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/**").authenticated() ) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) .build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ``` ```java // JwtAuthenticationFilter.java @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtService jwtService; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain chain ) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { chain.doFilter(request, response); return; } String jwt = authHeader.substring(7); String username = jwtService.extractUsername(jwt); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtService.isTokenValid(jwt, userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } } chain.doFilter(request, response); } } ``` Konfigurasi ini menghasilkan API stateless yang setiap permintaannya wajib menyertakan token JWT valid pada header Authorization. ### 17. Bagaimana mengamankan metode dengan @PreAuthorize dan @PostAuthorize? Keamanan tingkat metode memberi kendali halus berdasarkan role, izin, atau data. ```java // UserService.java @Service @PreAuthorize("isAuthenticated()") // All methods require authentication public class UserService { // Only ADMINs can list all users @PreAuthorize("hasRole('ADMIN')") public List findAll() { return userRepository.findAll(); } // User can view their profile OR admins can view any profile @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id") public User findById(Long id) { return userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException(id)); } // Post-execution check: user can only see their own data @PostAuthorize("returnObject.owner.id == authentication.principal.id or hasRole('ADMIN')") public Document getDocument(Long documentId) { return documentRepository.findById(documentId) .orElseThrow(() -> new DocumentNotFoundException(documentId)); } // Collection filtering: returns only authorized elements @PostFilter("filterObject.owner.id == authentication.principal.id") public List getUserProjects() { return projectRepository.findAll(); } } ``` ```java // SecurityConfig.java - Activation @Configuration @EnableMethodSecurity(prePostEnabled = true) public class MethodSecurityConfig { // Additional configuration if needed } ``` `@PreAuthorize` memeriksa sebelum eksekusi, `@PostAuthorize` memeriksa sesudahnya. Ekspresi SpEL memungkinkan aturan kompleks. ### 18. Bagaimana melindungi endpoint dari serangan CSRF? CSRF (Cross-Site Request Forgery) memanfaatkan sesi pengguna terotentikasi. Proteksi diaktifkan secara default untuk aplikasi berbasis sesi. ```java // SecurityConfig.java @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http // CSRF configuration for session-based applications .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) // Exclude certain endpoints from CSRF protection .ignoringRequestMatchers("/api/webhooks/**") ) .build(); } } ``` ```java // CsrfController.java - Endpoint to retrieve token (SPA) @RestController public class CsrfController { @GetMapping("/api/csrf") public CsrfToken csrf(CsrfToken token) { // Returns CSRF token to frontend return token; } } ``` Untuk REST API stateless dengan JWT, CSRF dapat dimatikan karena tidak ada cookie sesi yang dapat disalahgunakan. Proteksi sangat penting bagi aplikasi berbasis sesi tradisional. > **API stateless vs aplikasi berbasis sesi** > > API JWT secara alami terlindung dari CSRF karena token harus disertakan secara eksplisit pada setiap permintaan. Aplikasi dengan cookie sesi memerlukan proteksi CSRF aktif. ## Testing di Spring Boot ### 19. Apa beda @SpringBootTest dengan @WebMvcTest? Kedua anotasi ini menyiapkan konteks tes secara berbeda sesuai kebutuhan. ```java // UserControllerIntegrationTest.java @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class UserControllerIntegrationTest { // Loads full context: all beans, database, etc. @Autowired private TestRestTemplate restTemplate; @Test void shouldCreateUser() { CreateUserRequest request = new CreateUserRequest("John", "john@example.com"); ResponseEntity response = restTemplate.postForEntity( "/api/users", request, User.class ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(response.getBody().getName()).isEqualTo("John"); } } ``` ```java // UserControllerUnitTest.java @WebMvcTest(UserController.class) // Loads only the web layer class UserControllerUnitTest { @Autowired private MockMvc mockMvc; @MockitoBean // Replaces @MockBean (deprecated) private UserService userService; @Test void shouldReturnUser() throws Exception { // Mock configuration when(userService.findById(1L)) .thenReturn(Optional.of(new User(1L, "John", "john@example.com"))); mockMvc.perform(get("/api/users/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("John")) .andExpect(jsonPath("$.email").value("john@example.com")); } @Test void shouldReturn404WhenUserNotFound() throws Exception { when(userService.findById(999L)).thenReturn(Optional.empty()); mockMvc.perform(get("/api/users/999")) .andExpect(status().isNotFound()); } } ``` `@WebMvcTest` lebih cepat karena hanya memuat controller yang ditentukan beserta dependensinya. `@SpringBootTest` dibutuhkan untuk integrasi penuh. ### 20. Bagaimana menguji repository dengan @DataJpaTest? Anotasi ini menyiapkan konteks minimal untuk menguji lapisan persistensi dengan basis data tertanam. ```java // UserRepositoryTest.java @DataJpaTest @AutoConfigureTestDatabase(replace = Replace.NONE) // Use Testcontainers class UserRepositoryTest { @Autowired private UserRepository userRepository; @Autowired private TestEntityManager entityManager; @Test void shouldFindByEmail() { // Arrange: create and persist an entity User user = new User("John", "john@example.com"); entityManager.persistAndFlush(user); entityManager.clear(); // Clear L1 cache // Act Optional found = userRepository.findByEmail("john@example.com"); // Assert assertThat(found).isPresent(); assertThat(found.get().getName()).isEqualTo("John"); } @Test void shouldFindActiveUsersWithOrders() { // Testing a complex query with joins User activeUser = entityManager.persistAndFlush(new User("Active", "active@test.com", true)); User inactiveUser = entityManager.persistAndFlush(new User("Inactive", "inactive@test.com", false)); entityManager.persistAndFlush(new Order(activeUser, BigDecimal.valueOf(100))); List result = userRepository.findActiveUsersWithOrders(); assertThat(result).hasSize(1); assertThat(result.get(0).getEmail()).isEqualTo("active@test.com"); } } ``` `TestEntityManager` menyediakan utilitas untuk pengujian JPA, terutama `persistAndFlush()` yang memastikan data langsung tertulis ke basis data. ### 21. Bagaimana memakai Testcontainers untuk uji integrasi? Testcontainers menjalankan container Docker selama pengujian dengan basis data atau layanan nyata. ```java // IntegrationTestBase.java @SpringBootTest @Testcontainers abstract class IntegrationTestBase { @Container @ServiceConnection static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); @Container @ServiceConnection static RedisContainer redis = new RedisContainer("redis:7"); } ``` ```java // OrderServiceIntegrationTest.java class OrderServiceIntegrationTest extends IntegrationTestBase { @Autowired private OrderService orderService; @Autowired private OrderRepository orderRepository; @Test void shouldProcessOrderWithRealDatabase() { // Create an order Order order = orderService.createOrder( new CreateOrderRequest(1L, List.of( new OrderItem(101L, 2), new OrderItem(102L, 1) )) ); // Verify in database Order saved = orderRepository.findById(order.getId()).orElseThrow(); assertThat(saved.getItems()).hasSize(2); assertThat(saved.getStatus()).isEqualTo(OrderStatus.PENDING); } @Test void shouldCacheOrderInRedis() { Order order = orderService.createOrder(createSampleOrderRequest()); // First call: database Order fetched1 = orderService.findById(order.getId()); // Second call: Redis cache Order fetched2 = orderService.findById(order.getId()); assertThat(fetched1).isEqualTo(fetched2); // Verify cache metrics if needed } } ``` Anotasi `@ServiceConnection` mengatur properti koneksi secara otomatis sehingga `@DynamicPropertySource` tidak diperlukan. ## Konfigurasi dan monitoring ### 22. Bagaimana mengeksternalkan konfigurasi dengan @ConfigurationProperties? Anotasi ini memetakan properti ke objek Java terketik dengan validasi. ```java // MailProperties.java @ConfigurationProperties(prefix = "app.mail") @Validated public record MailProperties( @NotBlank String host, @Min(1) @Max(65535) int port, @NotBlank String username, @NotBlank String password, @Valid SenderConfig sender, @Valid RetryConfig retry ) { public record SenderConfig( @NotBlank @Email String address, @NotBlank String name ) {} public record RetryConfig( @Min(1) int maxAttempts, @Min(100) long delayMs ) { public RetryConfig { // Default values if (maxAttempts == 0) maxAttempts = 3; if (delayMs == 0) delayMs = 1000; } } } ``` ```yaml # application.yml app: mail: host: smtp.example.com port: 587 username: ${MAIL_USERNAME} password: ${MAIL_PASSWORD} sender: address: noreply@example.com name: MyApp Notifications retry: max-attempts: 3 delay-ms: 1000 ``` ```java // MailConfig.java @Configuration @EnableConfigurationProperties(MailProperties.class) public class MailConfig { @Bean public JavaMailSender mailSender(MailProperties props) { JavaMailSenderImpl sender = new JavaMailSenderImpl(); sender.setHost(props.host()); sender.setPort(props.port()); sender.setUsername(props.username()); sender.setPassword(props.password()); return sender; } } ``` Record Java (sejak Java 16) sangat cocok untuk `@ConfigurationProperties` karena bersifat immutable dan ringkas. ### 23. Bagaimana memakai profil Spring untuk lingkungan berbeda? Profil membantu menyesuaikan konfigurasi sesuai lingkungan eksekusi. ```yaml # application.yml - Default configuration spring: profiles: active: ${SPRING_PROFILES_ACTIVE:dev} datasource: url: ${DATABASE_URL:jdbc:h2:mem:testdb} --- # application-dev.yml spring: config: activate: on-profile: dev datasource: url: jdbc:postgresql://localhost:5432/myapp_dev jpa: show-sql: true hibernate: ddl-auto: update logging: level: com.example: DEBUG --- # application-prod.yml spring: config: activate: on-profile: prod datasource: url: ${DATABASE_URL} hikari: maximum-pool-size: 20 jpa: show-sql: false hibernate: ddl-auto: validate logging: level: com.example: INFO ``` ```java // DevDataInitializer.java @Component @Profile("dev") // Only active in development public class DevDataInitializer implements CommandLineRunner { private final UserRepository userRepository; @Override public void run(String... args) { // Create test data userRepository.save(new User("dev@example.com", "Dev User", "password")); userRepository.save(new User("test@example.com", "Test User", "password")); } } ``` ```java // Service with conditional behavior @Service public class NotificationService { @Value("${app.notifications.enabled:true}") private boolean notificationsEnabled; @Autowired(required = false) // Optional based on profile private EmailService emailService; public void sendNotification(String message) { if (notificationsEnabled && emailService != null) { emailService.send(message); } else { log.info("Notification (disabled): {}", message); } } } ``` Profil dapat digabung: `spring.profiles.active=prod,metrics` mengaktifkan keduanya sekaligus. ### 24. Bagaimana mempublikasikan metrik kustom dengan Actuator dan Micrometer? Micrometer menyediakan facade untuk sistem monitoring. Metrik kustom memperkaya observability. ```java // OrderMetrics.java @Component public class OrderMetrics { private final Counter ordersCreated; private final Counter ordersFailed; private final Timer orderProcessingTime; private final AtomicInteger activeOrders; public OrderMetrics(MeterRegistry registry) { // Counter for orders created by status this.ordersCreated = Counter.builder("orders.created") .description("Number of orders created") .tag("application", "order-service") .register(registry); this.ordersFailed = Counter.builder("orders.failed") .description("Number of failed orders") .register(registry); // Timer to measure processing time this.orderProcessingTime = Timer.builder("orders.processing.time") .description("Order processing time") .publishPercentiles(0.5, 0.95, 0.99) .register(registry); // Gauge for active orders count this.activeOrders = new AtomicInteger(0); Gauge.builder("orders.active", activeOrders, AtomicInteger::get) .description("Number of orders being processed") .register(registry); } public void recordOrderCreated() { ordersCreated.increment(); } public void recordOrderFailed() { ordersFailed.increment(); } public Timer.Sample startProcessing() { activeOrders.incrementAndGet(); return Timer.start(); } public void stopProcessing(Timer.Sample sample) { sample.stop(orderProcessingTime); activeOrders.decrementAndGet(); } } ``` ```java // OrderService.java @Service public class OrderService { private final OrderMetrics metrics; public Order processOrder(CreateOrderRequest request) { Timer.Sample sample = metrics.startProcessing(); try { Order order = createOrder(request); metrics.recordOrderCreated(); return order; } catch (Exception e) { metrics.recordOrderFailed(); throw e; } finally { metrics.stopProcessing(sample); } } } ``` Metrik tersebut tersedia melalui `/actuator/prometheus` untuk Prometheus atau `/actuator/metrics` untuk API JSON. ### 25. Bagaimana membuat HealthIndicator kustom? Indikator kesehatan memantau status komponen-komponen kritis. ```java // ExternalApiHealthIndicator.java @Component public class ExternalApiHealthIndicator implements HealthIndicator { private final RestClient restClient; private final ExternalApiProperties properties; @Override public Health health() { try { long startTime = System.currentTimeMillis(); ResponseEntity response = restClient.get() .uri(properties.getHealthEndpoint()) .retrieve() .toBodilessEntity(); long responseTime = System.currentTimeMillis() - startTime; if (response.getStatusCode().is2xxSuccessful()) { return Health.up() .withDetail("responseTime", responseTime + "ms") .withDetail("endpoint", properties.getHealthEndpoint()) .build(); } else { return Health.down() .withDetail("status", response.getStatusCode().value()) .build(); } } catch (Exception e) { return Health.down() .withException(e) .withDetail("endpoint", properties.getHealthEndpoint()) .build(); } } } ``` ```java // DatabaseHealthContributor.java - Composite health @Component public class DatabaseHealthContributor implements CompositeHealthContributor { private final Map indicators; public DatabaseHealthContributor( DataSource primaryDataSource, @Qualifier("replicaDataSource") DataSource replicaDataSource ) { this.indicators = Map.of( "primary", new DataSourceHealthIndicator(primaryDataSource), "replica", new DataSourceHealthIndicator(replicaDataSource) ); } @Override public HealthContributor getContributor(String name) { return indicators.get(name); } @Override public Iterator> iterator() { return indicators.entrySet().stream() .map(e -> NamedContributor.of(e.getKey(), e.getValue())) .iterator(); } } ``` Endpoint `/actuator/health` mengagregasi seluruh indikator. Properti `management.endpoint.health.show-details=always` akan menampilkan detailnya. ## Konsep lanjutan ### 26. Bagaimana mengimplementasikan caching dengan Spring Cache dan Redis? Abstraksi Spring Cache memudahkan integrasi cache terdistribusi seperti Redis. ```java // CacheConfig.java @Configuration @EnableCaching public class CacheConfig { @Bean public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10)) .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); // Specific configurations per cache Map cacheConfigs = Map.of( "users", config.entryTtl(Duration.ofHours(1)), "products", config.entryTtl(Duration.ofMinutes(30)), "sessions", config.entryTtl(Duration.ofMinutes(5)) ); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .withInitialCacheConfigurations(cacheConfigs) .build(); } } ``` ```java // ProductService.java @Service public class ProductService { // Automatic result caching @Cacheable(value = "products", key = "#id") public Product findById(Long id) { log.info("Fetching product {} from database", id); return productRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException(id)); } // Cache update on modification @CachePut(value = "products", key = "#product.id") public Product update(Product product) { return productRepository.save(product); } // Cache invalidation @CacheEvict(value = "products", key = "#id") public void delete(Long id) { productRepository.deleteById(id); } // Clear entire cache @CacheEvict(value = "products", allEntries = true) public void clearProductCache() { log.info("Product cache cleared"); } // Composite key for searches @Cacheable(value = "productSearch", key = "#category + '-' + #minPrice + '-' + #maxPrice") public List search(String category, BigDecimal minPrice, BigDecimal maxPrice) { return productRepository.findByCategoryAndPriceBetween(category, minPrice, maxPrice); } } ``` Anotasi cache bersifat transparan: kode bisnis tetap tidak berubah dan cache dikelola secara deklaratif. ### 27. Bagaimana mengimplementasikan tugas terjadwal dengan @Scheduled? Spring menyediakan beberapa cara untuk menjadwalkan tugas berulang. ```java // SchedulerConfig.java @Configuration @EnableScheduling public class SchedulerConfig { @Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); scheduler.setThreadNamePrefix("scheduled-"); scheduler.setErrorHandler(t -> log.error("Scheduled task error", t)); return scheduler; } } ``` ```java // ScheduledTasks.java @Component public class ScheduledTasks { private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class); // Execute every 5 minutes @Scheduled(fixedRate = 5 * 60 * 1000) public void syncExternalData() { log.info("Starting external data sync"); // Sync with external system } // Execute 10 seconds after the previous one ends @Scheduled(fixedDelay = 10000, initialDelay = 5000) public void processQueue() { // Queue processing with delay between executions } // Cron expression: every day at 2 AM @Scheduled(cron = "0 0 2 * * *", zone = "Europe/Paris") public void dailyCleanup() { log.info("Running daily cleanup"); cleanupService.removeExpiredSessions(); cleanupService.archiveOldData(); } // Configurable cron via properties @Scheduled(cron = "${app.reports.cron:0 0 6 * * MON}") public void generateWeeklyReport() { reportService.generateAndSend(); } } ``` ```java // ConditionalScheduling.java - Conditional activation @Component @ConditionalOnProperty(name = "app.scheduling.enabled", havingValue = "true") public class ConditionalScheduledTasks { @Scheduled(fixedRate = 60000) public void monitorSystem() { // Monitoring active only if configured } } ``` Tugas terjadwal berjalan pada thread pool terpisah agar tidak menghambat pemrosesan permintaan. ### 28. Bagaimana menangani event dengan ApplicationEvent? Sistem event memungkinkan komponen aplikasi tetap longgar. ```java // UserRegisteredEvent.java public class UserRegisteredEvent extends ApplicationEvent { private final User user; private final Instant registeredAt; public UserRegisteredEvent(Object source, User user) { super(source); this.user = user; this.registeredAt = Instant.now(); } public User getUser() { return user; } public Instant getRegisteredAt() { return registeredAt; } } ``` ```java // UserService.java - Event publishing @Service public class UserService { private final ApplicationEventPublisher eventPublisher; @Transactional public User registerUser(CreateUserRequest request) { User user = new User(request.email(), request.name()); user = userRepository.save(user); // Publish event after transaction eventPublisher.publishEvent(new UserRegisteredEvent(this, user)); return user; } } ``` ```java // UserEventListeners.java - Event listeners @Component public class UserEventListeners { private static final Logger log = LoggerFactory.getLogger(UserEventListeners.class); // Synchronous listener @EventListener public void handleUserRegistered(UserRegisteredEvent event) { log.info("User registered: {}", event.getUser().getEmail()); } // Asynchronous listener to avoid blocking the main thread @Async @EventListener public void sendWelcomeEmail(UserRegisteredEvent event) { emailService.sendWelcomeEmail(event.getUser()); } // Transactional listener: executes after commit @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void notifyExternalSystem(UserRegisteredEvent event) { // External notification only if transaction succeeds externalApiClient.notifyNewUser(event.getUser().getId()); } // Conditional listener with SpEL @EventListener(condition = "#event.user.role == 'PREMIUM'") public void handlePremiumUserRegistered(UserRegisteredEvent event) { premiumService.initializePremiumFeatures(event.getUser()); } } ``` Event transaksional (`@TransactionalEventListener`) memastikan konsistensi antara basis data dan efek samping. ### 29. Bagaimana mengimplementasikan klien HTTP dengan RestClient (Spring Boot 3+)? `RestClient` adalah API modern dan fluent untuk panggilan HTTP sinkron yang menggantikan `RestTemplate`. ```java // ExternalApiClient.java @Component public class ExternalApiClient { private final RestClient restClient; public ExternalApiClient(RestClient.Builder builder, ExternalApiProperties props) { this.restClient = builder .baseUrl(props.getBaseUrl()) .defaultHeader("Authorization", "Bearer " + props.getApiKey()) .defaultHeader("Accept", "application/json") .requestInterceptor((request, body, execution) -> { log.debug("Request: {} {}", request.getMethod(), request.getURI()); return execution.execute(request, body); }) .build(); } public User fetchUser(Long id) { return restClient.get() .uri("/users/{id}", id) .retrieve() .body(User.class); } public List searchProducts(String query, int page) { return restClient.get() .uri(uriBuilder -> uriBuilder .path("/products/search") .queryParam("q", query) .queryParam("page", page) .build()) .retrieve() .body(new ParameterizedTypeReference>() {}); } public Order createOrder(CreateOrderRequest request) { return restClient.post() .uri("/orders") .contentType(MediaType.APPLICATION_JSON) .body(request) .retrieve() .onStatus(HttpStatusCode::is4xxClientError, (req, res) -> { throw new OrderValidationException("Invalid order: " + res.getStatusCode()); }) .body(Order.class); } // Error handling with ResponseEntity public Optional findUser(Long id) { ResponseEntity response = restClient.get() .uri("/users/{id}", id) .retrieve() .toEntity(User.class); return response.getStatusCode().is2xxSuccessful() ? Optional.ofNullable(response.getBody()) : Optional.empty(); } } ``` `RestClient` menyediakan API yang mirip `WebClient` namun untuk panggilan blocking, ideal untuk aplikasi yang tidak menggunakan pemrograman reaktif. ### 30. Bagaimana mengelola migrasi basis data dengan Flyway? Flyway mengotomasi migrasi skema secara terversi dan dapat direproduksi. ```properties # application.properties spring.flyway.enabled=true spring.flyway.locations=classpath:db/migration spring.flyway.baseline-on-migrate=true spring.flyway.validate-on-migrate=true ``` ```sql -- db/migration/V1__create_users_table.sql CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, password_hash VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_users_email ON users(email); ``` ```sql -- db/migration/V2__add_user_status.sql ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'ACTIVE'; CREATE INDEX idx_users_status ON users(status); ``` ```sql -- db/migration/V3__create_orders_table.sql CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id), total_amount DECIMAL(10, 2) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'PENDING', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_orders_user_id ON orders(user_id); CREATE INDEX idx_orders_status ON orders(status); ``` ```java // FlywayConfig.java - Advanced configuration @Configuration public class FlywayConfig { @Bean public FlywayMigrationStrategy migrationStrategy() { return flyway -> { // Validation before migration flyway.validate(); // Execute migrations flyway.migrate(); }; } // Callback for post-migration actions @Bean public Callback flywayCallback() { return new BaseCallback() { @Override public void handle(Event event, Context context) { if (event == Event.AFTER_MIGRATE) { log.info("Migrations completed successfully"); } } }; } } ``` Setiap berkas migrasi hanya dijalankan satu kali, dan checksum-nya diperiksa untuk mendeteksi modifikasi yang tidak disengaja. ## Kesimpulan Ke-30 pertanyaan ini menjabarkan aspek penting Spring Boot yang dievaluasi dalam wawancara teknis. Menguasainya menunjukkan pemahaman mendalam terhadap framework dan praktik baik pengembangan Java. **Daftar persiapan:** - ✅ Memahami mekanisme auto-configuration beserta kondisinya - ✅ Menguasai Spring Data JPA: query, transaksi, masalah N+1 - ✅ Mampu mengonfigurasi Spring Security dengan JWT dan keamanan tingkat metode - ✅ Mengenal berbagai anotasi pengujian dan kasus penggunaannya - ✅ Menggunakan profil dan @ConfigurationProperties untuk konfigurasi - ✅ Mengimplementasikan caching, scheduling, dan event - ✅ Mempublikasikan metrik kustom dan indikator kesehatan - ✅ Menguasai RestClient untuk panggilan HTTP modern Latihan rutin dengan proyek nyata tetap menjadi cara terbaik untuk mengukuhkan pengetahuan ini dan menjawab pertanyaan teknis dengan percaya diri. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/spring-boot/30-spring-boot-interview-questions