30 Perguntas de Entrevista sobre Spring Boot: Guia Completo para Desenvolvedores Java
Prepare-se para suas entrevistas de Spring Boot com estas 30 perguntas essenciais sobre auto-configuração, starters, Spring Data JPA, segurança e testes.

As perguntas de entrevista sobre Spring Boot testam a compreensão dos mecanismos internos do framework, padrões de configuração e integração com o ecossistema Spring em geral. Com Spring Boot 4.1 como versão atual, os entrevistadores esperam que os candidatos conheçam tanto os conceitos fundamentais quanto as mudanças recentes. Este guia cobre 30 perguntas que vão dos fundamentos aos tópicos avançados.
Os entrevistadores valorizam candidatos que entendem o "porquê" de cada funcionalidade. Além da sintaxe, é preciso explicar quais problemas o Spring Boot resolve.
Fundamentos do Spring Boot
1. Qual é a diferença entre Spring e Spring Boot?
O Spring é um framework modular que oferece injeção de dependências, gerenciamento de transações e integração com diversas tecnologias. O Spring Boot é uma camada de abstração sobre o Spring que simplifica a configuração e a inicialização da aplicação.
// 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);
}
}O Spring Boot fornece auto-configuração, starters de dependências, servidor embutido e métricas de produção via Actuator. O objetivo é sair da ideia para o código funcional em poucos minutos.
2. Como funciona a auto-configuração?
A auto-configuração analisa o classpath e configura automaticamente os beans necessários. O mecanismo se apoia na anotação @EnableAutoConfiguration (incluída em @SpringBootApplication) e nas condições definidas em classes de auto-configuração.
@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();
}
}O Spring Boot examina as condições (@ConditionalOn*) para determinar quais beans criar. Essa abordagem evita conflitos e permite sobrescrever facilmente as configurações padrão.
3. O que é um starter e como criar um?
Um starter é um módulo Maven/Gradle que reúne dependências e configurações necessárias para uma funcionalidade. Por exemplo, spring-boot-starter-web inclui Spring MVC, Jackson, Tomcat embutido e validação.
<!-- pom.xml for a custom starter -->
<project>
<artifactId>my-company-starter</artifactId>
<dependencies>
<!-- Base Spring Boot dependency -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Company-specific libraries -->
<dependency>
<groupId>com.mycompany</groupId>
<artifactId>logging-utils</artifactId>
</dependency>
</dependencies>
</project>Para criar um starter customizado, basta definir classes de auto-configuração e referenciá-las em META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.
4. Explique application.properties vs application.yml
Ambos os formatos externalizam a configuração. O YAML oferece uma sintaxe mais legível para estruturas aninhadas, enquanto o properties continua mais simples para configurações planas.
# application.properties
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=admin
spring.jpa.hibernate.ddl-auto=update# application.yml - clear hierarchical structure
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: admin
jpa:
hibernate:
ddl-auto: updateOs profiles permitem adaptar a configuração por ambiente: application-dev.yml, application-prod.yml. A ativação acontece via spring.profiles.active.
5. Como funciona @SpringBootApplication?
Essa anotação combina três anotações essenciais que configuram o comportamento da aplicação Spring Boot.
// @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);
}
}Posicionar essa classe na raiz do pacote é fundamental: @ComponentScan varre esse pacote e todos os seus subpacotes em busca dos componentes.
Spring Data e persistência
6. Qual é a diferença entre JpaRepository, CrudRepository e PagingAndSortingRepository?
Essas interfaces formam uma hierarquia que oferece níveis crescentes de funcionalidades de acesso a dados. Para uma compreensão mais profunda das queries JPA, consulte o módulo de perguntas de entrevista sobre Spring Data JPA.
// CrudRepository: basic CRUD operations
public interface UserCrudRepository extends CrudRepository<User, Long> {
// save(), findById(), findAll(), deleteById(), count(), existsById()
}
// PagingAndSortingRepository: adds pagination and sorting
public interface UserPagingRepository extends PagingAndSortingRepository<User, Long> {
// Inherits from CrudRepository + findAll(Sort), findAll(Pageable)
}
// JpaRepository: complete JPA features
public interface UserRepository extends JpaRepository<User, Long> {
// Inherits from previous + flush(), saveAndFlush(), deleteInBatch()
// Derived query methods
List<User> findByEmailContaining(String email);
Optional<User> findByUsernameAndActiveTrue(String username);
}Na maioria dos casos, JpaRepository é a escolha recomendada porque oferece todas as funcionalidades necessárias para o desenvolvimento de aplicações JPA.
7. Como definir consultas customizadas com @Query?
A anotação @Query permite escrever consultas JPQL ou SQL nativas quando os métodos derivados não são suficientes.
public interface OrderRepository extends JpaRepository<Order, Long> {
// JPQL with named parameters
@Query("SELECT o FROM Order o WHERE o.customer.id = :customerId AND o.status = :status")
List<Order> 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<Order> 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<Long> ids, @Param("status") OrderStatus status);
}As consultas JPQL são portáveis entre bancos de dados, enquanto as consultas nativas dão acesso aos recursos específicos do SGBD.
8. Explique o gerenciamento de transações com @Transactional
Essa anotação declara os limites transacionais de um método ou classe. O Spring cuida automaticamente do commit, rollback e propagação. O guia de entrevista sobre propagação de transações cobre os níveis de propagação em detalhes.
@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));
}
}Os níveis de propagação (REQUIRED, REQUIRES_NEW, NESTED etc.) definem como as transações se aninham durante chamadas de métodos transacionais.
@Transactional só funciona em chamadas que passam pelo proxy do Spring. Uma chamada interna na mesma classe ignora o proxy e desconsidera a anotação.
9. Como tratar relacionamentos Lazy vs Eager Loading?
O modo de carregamento dos relacionamentos impacta diretamente o desempenho. Lazy Loading adia o carregamento até o acesso, Eager Loading carrega imediatamente.
@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<Role> roles;
// Lazy: orders are only loaded on access
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Order> orders;
}@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<OrderDTO> 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());
}
}O problema N+1 ocorre quando o lazy loading gera uma consulta por elemento da coleção. JOIN FETCH ou projeções resolvem o problema.
10. O que é o problema N+1 e como resolvê-lo?
O problema N+1 acontece quando uma consulta inicial (1) é seguida por N consultas adicionais para carregar os relacionamentos de cada entidade. O guia de soluções para o problema N+1 oferece uma cobertura completa deste tema.
public interface ArticleRepository extends JpaRepository<Article, Long> {
// PROBLEM: this query generates N+1 queries
List<Article> findAll();
// SOLUTION 1: JOIN FETCH in JPQL
@Query("SELECT a FROM Article a JOIN FETCH a.author JOIN FETCH a.comments")
List<Article> findAllWithDetails();
// SOLUTION 2: Entity Graph
@EntityGraph(attributePaths = {"author", "comments"})
@Query("SELECT a FROM Article a")
List<Article> findAllWithGraph();
}@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<Comment> comments;
}O uso de @EntityGraph ou JOIN FETCH reduz as consultas de N+1 a uma única consulta com joins, melhorando significativamente o desempenho.
Pronto para mandar bem nas entrevistas de Spring Boot?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
API REST e Web
11. Como criar um controller REST com validação?
O Spring Boot combina @RestController com Bean Validation para criar APIs robustas com validação automática das entradas.
@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<UserResponse> listUsers(
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "20") @Max(100) int size
) {
return userService.findAll(PageRequest.of(page, size));
}
}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
) {}As mensagens de validação podem ser externalizadas em arquivos de mensagens para internacionalização.
12. Como tratar exceções globalmente com @ControllerAdvice?
Um tratador centralizado de exceções uniformiza as respostas de erro e evita a duplicação de código.
@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<String, String> 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);
}
}public record ErrorResponse(
String code,
String message,
Map<String, String> details,
Instant timestamp
) {
public ErrorResponse(String code, String message, Map<String, String> details) {
this(code, message, details, Instant.now());
}
}Essa abordagem garante que todos os erros sigam o mesmo formato, facilitando o tratamento no lado cliente.
13. Qual é a diferença entre @RequestParam, @PathVariable e @RequestBody?
Essas três anotações extraem dados de partes diferentes da requisição HTTP.
@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<Product> 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 identifica um recurso específico, @RequestParam filtra ou pagina os resultados e @RequestBody transporta dados complexos.
14. Como implementar o versionamento de API?
Existem várias estratégias para versionar uma API REST, cada uma com suas vantagens.
// 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);
}
}// 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);
}
}// 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);
}
}O versionamento via URL continua sendo o mais popular pela simplicidade e visibilidade em logs e documentação.
15. Como configurar CORS no Spring Boot?
O CORS (Cross-Origin Resource Sharing) controla as requisições vindas de origens diferentes. Existem vários níveis de configuração.
@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
}
}// 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);
}
}Em produção, é recomendado limitar as origens permitidas e usar HTTPS. Nunca utilize * com allowCredentials(true).
Spring Security
16. Como configurar Spring Security com JWT?
O Spring Security 7 (incluído no Spring Boot 4.x) adota uma abordagem funcional para a configuração de segurança. O guia de autenticação JWT cobre os detalhes de implementação.
@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();
}
}@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);
}
}Essa configuração cria uma API stateless onde cada requisição precisa incluir um token JWT válido no cabeçalho Authorization.
17. Como proteger métodos com @PreAuthorize e @PostAuthorize?
A segurança em nível de método oferece controle granular baseado em papéis, permissões ou dados.
@Service
@PreAuthorize("isAuthenticated()") // All methods require authentication
public class UserService {
// Only ADMINs can list all users
@PreAuthorize("hasRole('ADMIN')")
public List<User> 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<Project> getUserProjects() {
return projectRepository.findAll();
}
}@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// Additional configuration if needed
}@PreAuthorize verifica antes da execução, @PostAuthorize depois. Expressões SpEL permitem regras complexas.
18. Como proteger endpoints contra ataques CSRF?
O CSRF (Cross-Site Request Forgery) explora a sessão de um usuário autenticado. No Spring Security 7 (Spring Boot 4.x), a proteção CSRF fica ativada por padrão para todos os endpoints, incluindo endpoints de API.
@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();
}
}@RestController
public class CsrfController {
@GetMapping("/api/csrf")
public CsrfToken csrf(CsrfToken token) {
// Returns CSRF token to frontend
return token;
}
}Para APIs REST stateless com JWT, o CSRF pode ser desativado já que não há cookie de sessão a ser explorado. A proteção é essencial para aplicações tradicionais baseadas em sessão.
O Spring Security 7 ativa a proteção CSRF por padrão mesmo para endpoints de API. APIs stateless com JWT devem desativá-la explicitamente com .csrf(csrf -> csrf.disable()). Aplicações migrando do Spring Boot 3.x devem verificar sua configuração de CSRF.
Testes em Spring Boot
19. Qual é a diferença entre @SpringBootTest e @WebMvcTest?
Essas anotações configuram o contexto de testes de formas diferentes conforme a necessidade. O Spring Boot 4.x removeu @MockBean em favor de @MockitoBean, e @SpringBootTest não configura mais o MockMvc automaticamente.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureTestRestTemplate // Required in Spring Boot 4.x
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<User> response = restTemplate.postForEntity(
"/api/users", request, User.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().getName()).isEqualTo("John");
}
}@WebMvcTest(UserController.class) // Loads only the web layer
class UserControllerUnitTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean // @MockBean was removed in Spring Boot 4.x
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 é mais rápido porque só carrega o controller especificado e suas dependências diretas. @SpringBootTest é necessário para testes de integração completos. O guia de integração com Testcontainers cobre testes realistas com bancos de dados.
20. Como testar repositórios com @DataJpaTest?
Essa anotação configura um contexto mínimo para testar a camada de persistência com um banco de dados embutido.
@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<User> 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<User> result = userRepository.findActiveUsersWithOrders();
assertThat(result).hasSize(1);
assertThat(result.get(0).getEmail()).isEqualTo("active@test.com");
}
}O TestEntityManager fornece métodos utilitários para os testes JPA, em especial persistAndFlush() que garante a escrita imediata no banco de dados.
21. Como usar Testcontainers para testes de integração?
O Testcontainers inicia containers Docker durante os testes com bancos de dados ou serviços reais.
@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");
}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
}
}A anotação @ServiceConnection configura automaticamente as propriedades de conexão, dispensando o uso de @DynamicPropertySource.
Pronto para mandar bem nas entrevistas de Spring Boot?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Configuração e monitoramento
22. Como externalizar a configuração com @ConfigurationProperties?
Essa anotação mapeia propriedades para objetos Java tipados com validação.
@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;
}
}
}# 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@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;
}
}Os records do Java (a partir do Java 16) são especialmente adequados para @ConfigurationProperties por serem imutáveis e concisos.
23. Como usar profiles do Spring para diferentes ambientes?
Os profiles permitem adaptar a configuração conforme o ambiente de execução.
# 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@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"));
}
}// 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);
}
}
}Os profiles podem ser combinados: spring.profiles.active=prod,metrics ativa os dois simultaneamente.
24. Como expor métricas customizadas com Actuator e Micrometer?
O Micrometer fornece uma fachada para sistemas de monitoramento. Métricas customizadas enriquecem a observabilidade. O guia de monitoramento com Actuator cobre a configuração em produção em detalhes.
@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();
}
}@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);
}
}
}Essas métricas ficam expostas em /actuator/prometheus para Prometheus ou /actuator/metrics para a API JSON.
25. Como criar um HealthIndicator customizado?
Indicadores de saúde monitoram o estado dos componentes críticos.
@Component
public class ExternalApiHealthIndicator implements HealthIndicator {
private final RestClient restClient;
private final ExternalApiProperties properties;
@Override
public Health health() {
try {
long startTime = System.currentTimeMillis();
ResponseEntity<Void> 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();
}
}
}@Component
public class DatabaseHealthContributor implements CompositeHealthContributor {
private final Map<String, HealthIndicator> 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<NamedContributor<HealthContributor>> iterator() {
return indicators.entrySet().stream()
.map(e -> NamedContributor.of(e.getKey(), e.getValue()))
.iterator();
}
}O endpoint /actuator/health agrega todos os indicadores. A configuração management.endpoint.health.show-details=always expõe os detalhes.
Conceitos avançados
26. Como implementar cache com Spring Cache e Redis?
A abstração Spring Cache simplifica a integração de caches distribuídos como o Redis.
@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<String, RedisCacheConfiguration> 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();
}
}@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<Product> search(String category, BigDecimal minPrice, BigDecimal maxPrice) {
return productRepository.findByCategoryAndPriceBetween(category, minPrice, maxPrice);
}
}As anotações de cache são transparentes: o código de negócio permanece inalterado e o cache é gerenciado de forma declarativa.
27. Como implementar uma tarefa agendada com @Scheduled?
O Spring oferece várias formas de agendar tarefas recorrentes.
@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;
}
}@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();
}
}@Component
@ConditionalOnProperty(name = "app.scheduling.enabled", havingValue = "true")
public class ConditionalScheduledTasks {
@Scheduled(fixedRate = 60000)
public void monitorSystem() {
// Monitoring active only if configured
}
}As tarefas agendadas rodam em um pool de threads separado para não bloquear o processamento das requisições.
28. Como tratar eventos com ApplicationEvent?
O sistema de eventos permite desacoplar os componentes da aplicação.
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; }
}@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;
}
}@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());
}
}Eventos transacionais (@TransactionalEventListener) garantem a consistência entre o banco de dados e os efeitos colaterais. No Spring Boot 4.1, a anotação @Async propaga automaticamente o contexto para threads separadas.
29. Como implementar um cliente HTTP com RestClient?
O RestClient é a API moderna e fluente para chamadas HTTP síncronas. O Spring Boot 4.x também introduziu o RestTestClient para cenários de testes.
@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<Product> 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<List<Product>>() {});
}
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<User> findUser(Long id) {
ResponseEntity<User> response = restClient.get()
.uri("/users/{id}", id)
.retrieve()
.toEntity(User.class);
return response.getStatusCode().is2xxSuccessful()
? Optional.ofNullable(response.getBody())
: Optional.empty();
}
}O RestClient oferece uma API parecida com o WebClient mas para chamadas bloqueantes, ideal para aplicações que não usam programação reativa.
30. Como gerenciar migrações de banco de dados com Flyway?
O Flyway automatiza as migrações de schema de forma versionada e reproduzível.
# application.properties
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=true
spring.flyway.validate-on-migrate=true-- 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);-- 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);-- 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);@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");
}
}
};
}
}Cada arquivo de migração é executado uma única vez e seu checksum é verificado para detectar modificações acidentais.
Sources
- Spring Boot 4.0 Release Announcement - lançamento da versão major com modularização e null safety com JSpecify
- Spring Boot 4.1 Release Notes - auto-configuração gRPC, mitigação SSRF, propagação de contexto para @Async
- Spring Boot 4.0 Migration Guide - remoção de @MockBean, mudanças em anotações de testes, migração para Jackson 3
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Preparação para entrevistas Spring Boot em 2026
Essas 30 perguntas cobrem os aspectos essenciais do Spring Boot avaliados em entrevistas técnicas. Com o Spring Boot 4.x como versão atual, entender tanto os conceitos fundamentais quanto as mudanças recentes demonstra preparo para o desenvolvimento Java moderno.
Pontos-chave para entrevistas:
- Mecanismo de auto-configuração e anotações @ConditionalOn*
- Padrões do Spring Data JPA: queries, transações, soluções N+1
- Configuração do Spring Security 7 com JWT e segurança em nível de método
- Anotações de testes: @MockitoBean (substituindo @MockBean), requisito de @AutoConfigureMockMvc
- Gerenciamento de configuração com profiles e @ConfigurationProperties
- Observabilidade com Actuator, Micrometer e indicadores de saúde customizados
- RestClient para chamadas HTTP (RestTemplate está obsoleto)
- Flyway para migrações de banco de dados versionadas
A prática constante em projetos reais continua sendo a melhor maneira de consolidar esse conhecimento e responder às perguntas técnicas com confiança.
Você saberia encontrar o bug em Spring Boot?
Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Escrito por
Anthony Fillion-MailletFundador da SharpSkill
Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.
Atualizado em 23 de agosto de 2026
Tags
Compartilhar
Artigos relacionados

Entrevista Spring Boot: Propagação de Transações
Domine a propagação de transações no Spring Boot: REQUIRED, REQUIRES_NEW, NESTED e mais. 12 perguntas de entrevista com código e armadilhas comuns.

Spring Boot 3.4: Todas as novidades explicadas
Spring Boot 3.4 traz logging estruturado nativo, virtual threads estendidos, graceful shutdown por padrão e MockMvcTester. Guia completo das novas funcionalidades.

Spring Modulith: Arquitetura de Monólito Modular Explicada
Aprenda Spring Modulith para construir monólitos modulares em Java. Arquitetura, módulos, eventos assíncronos, testes e observabilidade com exemplos em Spring Boot 3 e 4.