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.

O Spring Boot tornou-se o framework de referência para o desenvolvimento Java empresarial. As entrevistas técnicas avaliam a compreensão dos mecanismos internos, das boas práticas e do ecossistema Spring. Este guia cobre as 30 perguntas mais frequentes, dos conceitos fundamentais 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.
// 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.
@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.
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 6+ adota uma abordagem funcional para a configuração de segurança.
@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. A proteção fica ativa por padrão para aplicações baseadas em sessão.
@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.
APIs JWT são naturalmente protegidas contra CSRF porque o token precisa ser explicitamente incluído em cada requisição. Aplicações com cookies de sessão exigem proteção CSRF ativa.
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.
@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<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 // 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 é mais rápido porque só carrega o controller especificado e suas dependências diretas. @SpringBootTest é necessário para testes de integração completos.
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.
@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.
29. Como implementar um cliente HTTP com RestClient (Spring Boot 3+)?
O RestClient é a API moderna e fluente para chamadas HTTP síncronas, substituindo o RestTemplate.
@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.
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Conclusão
Essas 30 perguntas cobrem os aspectos essenciais do Spring Boot avaliados em entrevistas técnicas. Dominar esses conceitos demonstra um entendimento profundo do framework e das boas práticas de desenvolvimento Java.
Checklist de preparação:
- ✅ Entender o mecanismo de auto-configuração e suas condições
- ✅ Dominar Spring Data JPA: queries, transações, problema N+1
- ✅ Saber configurar Spring Security com JWT e segurança em nível de método
- ✅ Conhecer as diferentes anotações de teste e seus casos de uso
- ✅ Usar profiles e @ConfigurationProperties para a configuração
- ✅ Implementar cache, scheduling e eventos
- ✅ Expor métricas customizadas e indicadores de saúde
- ✅ Dominar o RestClient para chamadas HTTP modernas
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.
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 e testes com exemplos em Spring Boot 3.