# 30 Domande per Colloqui Spring Boot: Guida Completa per Sviluppatori Java
> Prepara i tuoi colloqui Spring Boot con queste 30 domande essenziali su auto-configurazione, starter, Spring Data JPA, sicurezza e testing.
- Published: 2026-02-07
- Updated: 2026-04-28
- Author: SharpSkill
- Tags: spring boot, java, interview, backend, java interview
- Reading time: 18 min
---
Spring Boot è diventato il framework di riferimento per lo sviluppo Java enterprise. I colloqui tecnici valutano la comprensione dei meccanismi interni, delle buone pratiche e dell'ecosistema Spring. Questa guida copre le 30 domande più frequenti, dai concetti fondamentali ai temi avanzati.
> **Consiglio di preparazione**
>
> Gli intervistatori apprezzano i candidati che capiscono il "perché" dietro ogni funzionalità. Oltre alla sintassi, è necessario spiegare quali problemi Spring Boot risolve.
## Fondamentali di Spring Boot
### 1. Qual è la differenza tra Spring e Spring Boot?
Spring è un framework modulare che offre dependency injection, gestione delle transazioni e integrazione con numerose tecnologie. Spring Boot è un livello di astrazione su Spring che semplifica la configurazione e l'avvio dell'applicazione.
```java
// Application.java
// With Spring Boot: a single annotation to start
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// SpringApplication configures and starts the Spring context
SpringApplication.run(Application.class, args);
}
}
```
Spring Boot fornisce auto-configurazione, starter di dipendenze, server embedded e metriche di produzione tramite Actuator. L'obiettivo è passare dall'idea al codice funzionante in pochi minuti.
### 2. Come funziona l'auto-configurazione?
L'auto-configurazione analizza il classpath e configura automaticamente i bean necessari. Il meccanismo si basa sull'annotazione `@EnableAutoConfiguration` (inclusa in `@SpringBootApplication`) e su condizioni definite in classi di auto-configurazione.
```java
// CustomAutoConfiguration.java
@Configuration
@ConditionalOnClass(DataSource.class) // Activates if DataSource is on classpath
@ConditionalOnMissingBean(DataSource.class) // Only acts if no DataSource exists
public class CustomAutoConfiguration {
@Bean
@ConditionalOnProperty(name = "app.datasource.enabled", havingValue = "true")
public DataSource dataSource() {
// Default DataSource configuration
return DataSourceBuilder.create()
.driverClassName("org.h2.Driver")
.url("jdbc:h2:mem:testdb")
.build();
}
}
```
Spring Boot esamina le condizioni (`@ConditionalOn*`) per decidere quali bean creare. Questo approccio evita conflitti e permette di sovrascrivere facilmente le configurazioni di default.
### 3. Cos'è uno starter e come crearne uno?
Uno starter è un modulo Maven/Gradle che raggruppa le dipendenze e le configurazioni necessarie per una funzionalità. Per esempio, `spring-boot-starter-web` include Spring MVC, Jackson, Tomcat embedded e validation.
```xml
my-company-starter
org.springframework.boot
spring-boot-starter
com.mycompany
logging-utils
```
Per creare uno starter personalizzato, basta definire classi di auto-configurazione e referenziarle in `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`.
### 4. Spiega application.properties vs application.yml
Entrambi i formati esternalizzano la configurazione. YAML offre una sintassi più leggibile per le strutture annidate, mentre properties resta più semplice per configurazioni piatte.
```properties
# application.properties
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=admin
spring.jpa.hibernate.ddl-auto=update
```
```yaml
# application.yml - clear hierarchical structure
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: admin
jpa:
hibernate:
ddl-auto: update
```
I profili adattano la configurazione in base all'ambiente: `application-dev.yml`, `application-prod.yml`. L'attivazione avviene tramite `spring.profiles.active`.
### 5. Come funziona @SpringBootApplication?
Questa annotazione combina tre annotazioni essenziali che configurano il comportamento dell'applicazione Spring Boot.
```java
// DemoApplication.java
// @SpringBootApplication is equivalent to these three annotations:
@SpringBootConfiguration // Equivalent to @Configuration
@EnableAutoConfiguration // Activates auto-configuration
@ComponentScan // Scans components in package and sub-packages
public class DemoApplication {
public static void main(String[] args) {
// Creates context, runs configurations, and starts server
ConfigurableApplicationContext context =
SpringApplication.run(DemoApplication.class, args);
// The context contains all configured beans
UserService userService = context.getBean(UserService.class);
}
}
```
Posizionare questa classe nella radice del package è cruciale: `@ComponentScan` esamina questo package e tutti i sotto-package alla ricerca dei componenti.
## Spring Data e persistenza
### 6. Qual è la differenza tra JpaRepository, CrudRepository e PagingAndSortingRepository?
Queste interfacce formano una gerarchia che offre livelli crescenti di funzionalità per l'accesso ai dati.
```java
// UserRepository.java
// CrudRepository: basic CRUD operations
public interface UserCrudRepository extends CrudRepository {
// save(), findById(), findAll(), deleteById(), count(), existsById()
}
// PagingAndSortingRepository: adds pagination and sorting
public interface UserPagingRepository extends PagingAndSortingRepository {
// Inherits from CrudRepository + findAll(Sort), findAll(Pageable)
}
// JpaRepository: complete JPA features
public interface UserRepository extends JpaRepository {
// Inherits from previous + flush(), saveAndFlush(), deleteInBatch()
// Derived query methods
List findByEmailContaining(String email);
Optional findByUsernameAndActiveTrue(String username);
}
```
Nella maggior parte dei casi, `JpaRepository` è la scelta consigliata perché offre tutte le funzionalità necessarie per lo sviluppo di applicazioni JPA.
### 7. Come si definiscono query personalizzate con @Query?
L'annotazione `@Query` permette di scrivere query JPQL o SQL native quando i metodi derivati non sono sufficienti.
```java
// OrderRepository.java
public interface OrderRepository extends JpaRepository {
// JPQL with named parameters
@Query("SELECT o FROM Order o WHERE o.customer.id = :customerId AND o.status = :status")
List findCustomerOrdersByStatus(
@Param("customerId") Long customerId,
@Param("status") OrderStatus status
);
// Native SQL query for specific needs
@Query(value = "SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '7 days'",
nativeQuery = true)
List findRecentOrders();
// Modifying query with @Modifying
@Modifying
@Transactional
@Query("UPDATE Order o SET o.status = :status WHERE o.id IN :ids")
int updateOrderStatuses(@Param("ids") List ids, @Param("status") OrderStatus status);
}
```
Le query JPQL sono portabili tra database, mentre le query native danno accesso alle funzionalità specifiche del DBMS.
### 8. Spiega la gestione delle transazioni con @Transactional
Questa annotazione dichiara i confini transazionali di un metodo o di una classe. Spring gestisce automaticamente commit, rollback e propagazione.
```java
// PaymentService.java
@Service
public class PaymentService {
private final AccountRepository accountRepository;
private final TransactionLogRepository logRepository;
// Transaction with custom configuration
@Transactional(
propagation = Propagation.REQUIRED, // Creates or joins a transaction
isolation = Isolation.READ_COMMITTED, // Isolation level
timeout = 30, // Timeout in seconds
rollbackFor = PaymentException.class // Rollback on this exception
)
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId)
.orElseThrow(() -> new AccountNotFoundException(fromId));
Account to = accountRepository.findById(toId)
.orElseThrow(() -> new AccountNotFoundException(toId));
from.debit(amount); // Throws exception if insufficient balance
to.credit(amount);
accountRepository.save(from);
accountRepository.save(to);
// Log will be rolled back with everything else if an exception occurs
logRepository.save(new TransactionLog(fromId, toId, amount));
}
}
```
I livelli di propagazione (`REQUIRED`, `REQUIRES_NEW`, `NESTED`, ecc.) definiscono il modo in cui le transazioni si annidano durante le chiamate a metodi transazionali.
> **Trabocchetto comune**
>
> `@Transactional` funziona solo per le chiamate che passano dal proxy di Spring. Una chiamata interna nella stessa classe bypassa il proxy e ignora l'annotazione.
### 9. Come gestire le relazioni Lazy vs Eager Loading?
La modalità di caricamento delle relazioni incide direttamente sulle performance. Lazy Loading rimanda il caricamento al momento dell'accesso, Eager Loading carica subito.
```java
// User.java
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Eager: roles are loaded with the user
@ManyToMany(fetch = FetchType.EAGER)
private Set roles;
// Lazy: orders are only loaded on access
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List orders;
}
```
```java
// UserService.java
@Service
public class UserService {
@Transactional(readOnly = true)
public UserDTO getUserWithOrders(Long userId) {
User user = userRepository.findById(userId).orElseThrow();
// Accessing orders triggers an additional SQL query
// This works because the session is open (@Transactional)
List orderDTOs = user.getOrders().stream()
.map(this::toDTO)
.collect(Collectors.toList());
return new UserDTO(user, orderDTOs);
}
// Alternative: query with JOIN FETCH to avoid N+1
public UserDTO getUserWithOrdersOptimized(Long userId) {
User user = userRepository.findByIdWithOrders(userId);
// Orders are already loaded, no additional query
return new UserDTO(user, user.getOrders());
}
}
```
Il problema N+1 si verifica quando il lazy loading genera una query per ogni elemento di una collezione. `JOIN FETCH` o le proiezioni risolvono il problema.
### 10. Cos'è il problema N+1 e come si risolve?
Il problema N+1 si presenta quando una query iniziale (1) è seguita da N query aggiuntive per caricare le relazioni di ogni entità.
```java
// ArticleRepository.java
public interface ArticleRepository extends JpaRepository {
// PROBLEM: this query generates N+1 queries
List findAll();
// SOLUTION 1: JOIN FETCH in JPQL
@Query("SELECT a FROM Article a JOIN FETCH a.author JOIN FETCH a.comments")
List findAllWithDetails();
// SOLUTION 2: Entity Graph
@EntityGraph(attributePaths = {"author", "comments"})
@Query("SELECT a FROM Article a")
List findAllWithGraph();
}
```
```java
// Article.java
@Entity
@NamedEntityGraph(
name = "Article.withDetails",
attributeNodes = {
@NamedAttributeNode("author"),
@NamedAttributeNode("comments")
}
)
public class Article {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Author author;
@OneToMany(mappedBy = "article", fetch = FetchType.LAZY)
private List comments;
}
```
L'uso di `@EntityGraph` o `JOIN FETCH` riduce le query da N+1 a una singola query con join, migliorando in modo significativo le performance.
## API REST e Web
### 11. Come creare un controller REST con validazione?
Spring Boot combina `@RestController` con Bean Validation per creare API robuste con validazione automatica degli input.
```java
// UserController.java
@RestController
@RequestMapping("/api/users")
@Validated // Activates validation on method parameters
public class UserController {
private final UserService userService;
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserResponse createUser(@Valid @RequestBody CreateUserRequest request) {
// Validation is performed automatically before execution
return userService.createUser(request);
}
@GetMapping("/{id}")
public UserResponse getUser(
@PathVariable @Min(1) Long id // Validation on PathVariable
) {
return userService.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
@GetMapping
public Page listUsers(
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "20") @Max(100) int size
) {
return userService.findAll(PageRequest.of(page, size));
}
}
```
```java
// CreateUserRequest.java
public record CreateUserRequest(
@NotBlank(message = "Name is required")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
String name,
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
String email,
@NotBlank
@Pattern(regexp = "^(?=.*[A-Z])(?=.*[0-9]).{8,}$",
message = "Password must contain at least 8 characters, one uppercase and one digit")
String password
) {}
```
I messaggi di validazione possono essere esternalizzati in file di messaggi per l'internazionalizzazione.
### 12. Come gestire le eccezioni globalmente con @ControllerAdvice?
Un gestore centralizzato di eccezioni uniforma le risposte di errore ed evita la duplicazione del codice.
```java
// GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// Handle validation errors
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidationErrors(MethodArgumentNotValidException ex) {
Map errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage,
(existing, replacement) -> existing
));
return new ErrorResponse("VALIDATION_ERROR", "Invalid data", errors);
}
// Handle resource not found
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
return new ErrorResponse("NOT_FOUND", ex.getMessage(), null);
}
// Handle unexpected errors
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleUnexpectedError(Exception ex) {
log.error("Unexpected error", ex);
return new ErrorResponse("INTERNAL_ERROR", "An error occurred", null);
}
}
```
```java
// ErrorResponse.java
public record ErrorResponse(
String code,
String message,
Map details,
Instant timestamp
) {
public ErrorResponse(String code, String message, Map details) {
this(code, message, details, Instant.now());
}
}
```
Questo approccio garantisce che tutti gli errori seguano lo stesso formato, semplificando l'elaborazione lato client.
### 13. Qual è la differenza tra @RequestParam, @PathVariable e @RequestBody?
Queste tre annotazioni estraggono dati da diverse parti della richiesta HTTP.
```java
// ProductController.java
@RestController
@RequestMapping("/api/products")
public class ProductController {
// @PathVariable: extracts values from the URL
// GET /api/products/123
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productService.findById(id);
}
// @RequestParam: extracts query parameters
// GET /api/products?category=electronics&minPrice=100&page=0
@GetMapping
public Page searchProducts(
@RequestParam(required = false) String category,
@RequestParam(defaultValue = "0") BigDecimal minPrice,
@RequestParam(defaultValue = "0") int page
) {
return productService.search(category, minPrice, PageRequest.of(page, 20));
}
// @RequestBody: deserializes the JSON request body
// POST /api/products with {"name": "...", "price": ...}
@PostMapping
public Product createProduct(@Valid @RequestBody CreateProductRequest request) {
return productService.create(request);
}
// Combination of all three in the same method
// PUT /api/products/123?notify=true with JSON body
@PutMapping("/{id}")
public Product updateProduct(
@PathVariable Long id,
@RequestParam(defaultValue = "false") boolean notify,
@Valid @RequestBody UpdateProductRequest request
) {
Product updated = productService.update(id, request);
if (notify) {
notificationService.sendUpdateNotification(updated);
}
return updated;
}
}
```
`@PathVariable` identifica una risorsa specifica, `@RequestParam` filtra o pagina i risultati e `@RequestBody` trasporta dati complessi.
### 14. Come implementare il versioning dell'API?
Esistono diverse strategie per versionare un'API REST, ciascuna con i propri vantaggi.
```java
// Version via URL (recommended for clarity)
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
@GetMapping("/{id}")
public UserV1Response getUser(@PathVariable Long id) {
return userService.findByIdV1(id);
}
}
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
@GetMapping("/{id}")
public UserV2Response getUser(@PathVariable Long id) {
// V2 includes additional fields
return userService.findByIdV2(id);
}
}
```
```java
// Version via custom header
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping(value = "/{id}", headers = "X-API-Version=1")
public UserV1Response getUserV1(@PathVariable Long id) {
return userService.findByIdV1(id);
}
@GetMapping(value = "/{id}", headers = "X-API-Version=2")
public UserV2Response getUserV2(@PathVariable Long id) {
return userService.findByIdV2(id);
}
}
```
```java
// Version via media type (content negotiation)
@RestController
@RequestMapping("/api/users")
public class UserMediaTypeController {
@GetMapping(value = "/{id}", produces = "application/vnd.myapi.v1+json")
public UserV1Response getUserV1(@PathVariable Long id) {
return userService.findByIdV1(id);
}
@GetMapping(value = "/{id}", produces = "application/vnd.myapi.v2+json")
public UserV2Response getUserV2(@PathVariable Long id) {
return userService.findByIdV2(id);
}
}
```
Il versioning via URL resta il più diffuso per la sua semplicità e visibilità nei log e nella documentazione.
### 15. Come configurare CORS in Spring Boot?
CORS (Cross-Origin Resource Sharing) governa le richieste provenienti da origini diverse. Sono disponibili diversi livelli di configurazione.
```java
// CorsConfig.java - Global configuration
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://frontend.example.com", "https://admin.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("X-Total-Count", "X-Page-Count")
.allowCredentials(true)
.maxAge(3600); // Cache preflight for 1 hour
}
}
```
```java
// Per-controller or method configuration
@RestController
@RequestMapping("/api/public")
@CrossOrigin(origins = "*", maxAge = 3600)
public class PublicApiController {
@GetMapping("/data")
public DataResponse getPublicData() {
return dataService.getPublicData();
}
// Override at method level
@CrossOrigin(origins = "https://specific-client.com")
@PostMapping("/submit")
public SubmitResponse submitData(@RequestBody SubmitRequest request) {
return dataService.submit(request);
}
}
```
In produzione conviene limitare le origini consentite e usare HTTPS. Mai utilizzare `*` insieme a `allowCredentials(true)`.
## Spring Security
### 16. Come configurare Spring Security con JWT?
Spring Security 6+ adotta un approccio funzionale per la configurazione della sicurezza.
```java
// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // Disabled for stateless API
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
```
```java
// JwtAuthenticationFilter.java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
String jwt = authHeader.substring(7);
String username = jwtService.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
chain.doFilter(request, response);
}
}
```
Questa configurazione crea un'API stateless in cui ogni richiesta deve includere un token JWT valido nell'header Authorization.
### 17. Come proteggere i metodi con @PreAuthorize e @PostAuthorize?
La sicurezza a livello di metodo offre un controllo granulare basato su ruoli, permessi o dati.
```java
// UserService.java
@Service
@PreAuthorize("isAuthenticated()") // All methods require authentication
public class UserService {
// Only ADMINs can list all users
@PreAuthorize("hasRole('ADMIN')")
public List findAll() {
return userRepository.findAll();
}
// User can view their profile OR admins can view any profile
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
// Post-execution check: user can only see their own data
@PostAuthorize("returnObject.owner.id == authentication.principal.id or hasRole('ADMIN')")
public Document getDocument(Long documentId) {
return documentRepository.findById(documentId)
.orElseThrow(() -> new DocumentNotFoundException(documentId));
}
// Collection filtering: returns only authorized elements
@PostFilter("filterObject.owner.id == authentication.principal.id")
public List getUserProjects() {
return projectRepository.findAll();
}
}
```
```java
// SecurityConfig.java - Activation
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// Additional configuration if needed
}
```
`@PreAuthorize` verifica prima dell'esecuzione, `@PostAuthorize` dopo. Le espressioni SpEL permettono regole complesse.
### 18. Come proteggere gli endpoint dagli attacchi CSRF?
CSRF (Cross-Site Request Forgery) sfrutta la sessione di un utente autenticato. La protezione è attiva di default per le applicazioni basate su sessione.
```java
// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// CSRF configuration for session-based applications
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
// Exclude certain endpoints from CSRF protection
.ignoringRequestMatchers("/api/webhooks/**")
)
.build();
}
}
```
```java
// CsrfController.java - Endpoint to retrieve token (SPA)
@RestController
public class CsrfController {
@GetMapping("/api/csrf")
public CsrfToken csrf(CsrfToken token) {
// Returns CSRF token to frontend
return token;
}
}
```
Per le API REST stateless con JWT, il CSRF può essere disabilitato perché non c'è un cookie di sessione da sfruttare. La protezione è invece essenziale per le applicazioni tradizionali basate su sessione.
> **API stateless vs applicazioni con sessione**
>
> Le API JWT sono naturalmente protette dal CSRF perché il token deve essere incluso esplicitamente in ogni richiesta. Le applicazioni con cookie di sessione richiedono una protezione CSRF attiva.
## Testing in Spring Boot
### 19. Qual è la differenza tra @SpringBootTest e @WebMvcTest?
Queste annotazioni configurano il contesto di test in modo diverso a seconda delle esigenze.
```java
// UserControllerIntegrationTest.java
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {
// Loads full context: all beans, database, etc.
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldCreateUser() {
CreateUserRequest request = new CreateUserRequest("John", "john@example.com");
ResponseEntity response = restTemplate.postForEntity(
"/api/users", request, User.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().getName()).isEqualTo("John");
}
}
```
```java
// UserControllerUnitTest.java
@WebMvcTest(UserController.class) // Loads only the web layer
class UserControllerUnitTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean // Replaces @MockBean (deprecated)
private UserService userService;
@Test
void shouldReturnUser() throws Exception {
// Mock configuration
when(userService.findById(1L))
.thenReturn(Optional.of(new User(1L, "John", "john@example.com")));
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John"))
.andExpect(jsonPath("$.email").value("john@example.com"));
}
@Test
void shouldReturn404WhenUserNotFound() throws Exception {
when(userService.findById(999L)).thenReturn(Optional.empty());
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
}
}
```
`@WebMvcTest` è più rapido perché carica soltanto il controller specificato e le sue dipendenze dirette. `@SpringBootTest` è necessario per i test di integrazione completi.
### 20. Come testare i repository con @DataJpaTest?
Questa annotazione configura un contesto minimo per testare il livello di persistenza con un database embedded.
```java
// UserRepositoryTest.java
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE) // Use Testcontainers
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldFindByEmail() {
// Arrange: create and persist an entity
User user = new User("John", "john@example.com");
entityManager.persistAndFlush(user);
entityManager.clear(); // Clear L1 cache
// Act
Optional found = userRepository.findByEmail("john@example.com");
// Assert
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("John");
}
@Test
void shouldFindActiveUsersWithOrders() {
// Testing a complex query with joins
User activeUser = entityManager.persistAndFlush(new User("Active", "active@test.com", true));
User inactiveUser = entityManager.persistAndFlush(new User("Inactive", "inactive@test.com", false));
entityManager.persistAndFlush(new Order(activeUser, BigDecimal.valueOf(100)));
List result = userRepository.findActiveUsersWithOrders();
assertThat(result).hasSize(1);
assertThat(result.get(0).getEmail()).isEqualTo("active@test.com");
}
}
```
`TestEntityManager` mette a disposizione metodi di utilità per i test JPA, in particolare `persistAndFlush()` che garantisce la scrittura immediata sul database.
### 21. Come usare Testcontainers per i test di integrazione?
Testcontainers avvia container Docker durante i test con database o servizi reali.
```java
// IntegrationTestBase.java
@SpringBootTest
@Testcontainers
abstract class IntegrationTestBase {
@Container
@ServiceConnection
static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:16")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Container
@ServiceConnection
static RedisContainer redis = new RedisContainer("redis:7");
}
```
```java
// OrderServiceIntegrationTest.java
class OrderServiceIntegrationTest extends IntegrationTestBase {
@Autowired
private OrderService orderService;
@Autowired
private OrderRepository orderRepository;
@Test
void shouldProcessOrderWithRealDatabase() {
// Create an order
Order order = orderService.createOrder(
new CreateOrderRequest(1L, List.of(
new OrderItem(101L, 2),
new OrderItem(102L, 1)
))
);
// Verify in database
Order saved = orderRepository.findById(order.getId()).orElseThrow();
assertThat(saved.getItems()).hasSize(2);
assertThat(saved.getStatus()).isEqualTo(OrderStatus.PENDING);
}
@Test
void shouldCacheOrderInRedis() {
Order order = orderService.createOrder(createSampleOrderRequest());
// First call: database
Order fetched1 = orderService.findById(order.getId());
// Second call: Redis cache
Order fetched2 = orderService.findById(order.getId());
assertThat(fetched1).isEqualTo(fetched2);
// Verify cache metrics if needed
}
}
```
L'annotazione `@ServiceConnection` configura automaticamente le proprietà di connessione, eliminando la necessità di `@DynamicPropertySource`.
## Configurazione e monitoraggio
### 22. Come esternalizzare la configurazione con @ConfigurationProperties?
Questa annotazione mappa le properties su oggetti Java tipizzati con validazione.
```java
// MailProperties.java
@ConfigurationProperties(prefix = "app.mail")
@Validated
public record MailProperties(
@NotBlank String host,
@Min(1) @Max(65535) int port,
@NotBlank String username,
@NotBlank String password,
@Valid SenderConfig sender,
@Valid RetryConfig retry
) {
public record SenderConfig(
@NotBlank @Email String address,
@NotBlank String name
) {}
public record RetryConfig(
@Min(1) int maxAttempts,
@Min(100) long delayMs
) {
public RetryConfig {
// Default values
if (maxAttempts == 0) maxAttempts = 3;
if (delayMs == 0) delayMs = 1000;
}
}
}
```
```yaml
# application.yml
app:
mail:
host: smtp.example.com
port: 587
username: ${MAIL_USERNAME}
password: ${MAIL_PASSWORD}
sender:
address: noreply@example.com
name: MyApp Notifications
retry:
max-attempts: 3
delay-ms: 1000
```
```java
// MailConfig.java
@Configuration
@EnableConfigurationProperties(MailProperties.class)
public class MailConfig {
@Bean
public JavaMailSender mailSender(MailProperties props) {
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost(props.host());
sender.setPort(props.port());
sender.setUsername(props.username());
sender.setPassword(props.password());
return sender;
}
}
```
I record di Java (da Java 16) sono particolarmente adatti a `@ConfigurationProperties` perché immutabili e concisi.
### 23. Come usare i profili Spring per ambienti diversi?
I profili adattano la configurazione in base all'ambiente di esecuzione.
```yaml
# application.yml - Default configuration
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
datasource:
url: ${DATABASE_URL:jdbc:h2:mem:testdb}
---
# application-dev.yml
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:postgresql://localhost:5432/myapp_dev
jpa:
show-sql: true
hibernate:
ddl-auto: update
logging:
level:
com.example: DEBUG
---
# application-prod.yml
spring:
config:
activate:
on-profile: prod
datasource:
url: ${DATABASE_URL}
hikari:
maximum-pool-size: 20
jpa:
show-sql: false
hibernate:
ddl-auto: validate
logging:
level:
com.example: INFO
```
```java
// DevDataInitializer.java
@Component
@Profile("dev") // Only active in development
public class DevDataInitializer implements CommandLineRunner {
private final UserRepository userRepository;
@Override
public void run(String... args) {
// Create test data
userRepository.save(new User("dev@example.com", "Dev User", "password"));
userRepository.save(new User("test@example.com", "Test User", "password"));
}
}
```
```java
// Service with conditional behavior
@Service
public class NotificationService {
@Value("${app.notifications.enabled:true}")
private boolean notificationsEnabled;
@Autowired(required = false) // Optional based on profile
private EmailService emailService;
public void sendNotification(String message) {
if (notificationsEnabled && emailService != null) {
emailService.send(message);
} else {
log.info("Notification (disabled): {}", message);
}
}
}
```
I profili possono essere combinati: `spring.profiles.active=prod,metrics` attiva entrambi contemporaneamente.
### 24. Come esporre metriche personalizzate con Actuator e Micrometer?
Micrometer fornisce una facade per i sistemi di monitoraggio. Le metriche personalizzate arricchiscono l'osservabilità.
```java
// OrderMetrics.java
@Component
public class OrderMetrics {
private final Counter ordersCreated;
private final Counter ordersFailed;
private final Timer orderProcessingTime;
private final AtomicInteger activeOrders;
public OrderMetrics(MeterRegistry registry) {
// Counter for orders created by status
this.ordersCreated = Counter.builder("orders.created")
.description("Number of orders created")
.tag("application", "order-service")
.register(registry);
this.ordersFailed = Counter.builder("orders.failed")
.description("Number of failed orders")
.register(registry);
// Timer to measure processing time
this.orderProcessingTime = Timer.builder("orders.processing.time")
.description("Order processing time")
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry);
// Gauge for active orders count
this.activeOrders = new AtomicInteger(0);
Gauge.builder("orders.active", activeOrders, AtomicInteger::get)
.description("Number of orders being processed")
.register(registry);
}
public void recordOrderCreated() {
ordersCreated.increment();
}
public void recordOrderFailed() {
ordersFailed.increment();
}
public Timer.Sample startProcessing() {
activeOrders.incrementAndGet();
return Timer.start();
}
public void stopProcessing(Timer.Sample sample) {
sample.stop(orderProcessingTime);
activeOrders.decrementAndGet();
}
}
```
```java
// OrderService.java
@Service
public class OrderService {
private final OrderMetrics metrics;
public Order processOrder(CreateOrderRequest request) {
Timer.Sample sample = metrics.startProcessing();
try {
Order order = createOrder(request);
metrics.recordOrderCreated();
return order;
} catch (Exception e) {
metrics.recordOrderFailed();
throw e;
} finally {
metrics.stopProcessing(sample);
}
}
}
```
Queste metriche sono esposte via `/actuator/prometheus` per Prometheus o `/actuator/metrics` per l'API JSON.
### 25. Come creare un HealthIndicator personalizzato?
Gli indicatori di salute monitorano lo stato dei componenti critici.
```java
// ExternalApiHealthIndicator.java
@Component
public class ExternalApiHealthIndicator implements HealthIndicator {
private final RestClient restClient;
private final ExternalApiProperties properties;
@Override
public Health health() {
try {
long startTime = System.currentTimeMillis();
ResponseEntity response = restClient.get()
.uri(properties.getHealthEndpoint())
.retrieve()
.toBodilessEntity();
long responseTime = System.currentTimeMillis() - startTime;
if (response.getStatusCode().is2xxSuccessful()) {
return Health.up()
.withDetail("responseTime", responseTime + "ms")
.withDetail("endpoint", properties.getHealthEndpoint())
.build();
} else {
return Health.down()
.withDetail("status", response.getStatusCode().value())
.build();
}
} catch (Exception e) {
return Health.down()
.withException(e)
.withDetail("endpoint", properties.getHealthEndpoint())
.build();
}
}
}
```
```java
// DatabaseHealthContributor.java - Composite health
@Component
public class DatabaseHealthContributor implements CompositeHealthContributor {
private final Map indicators;
public DatabaseHealthContributor(
DataSource primaryDataSource,
@Qualifier("replicaDataSource") DataSource replicaDataSource
) {
this.indicators = Map.of(
"primary", new DataSourceHealthIndicator(primaryDataSource),
"replica", new DataSourceHealthIndicator(replicaDataSource)
);
}
@Override
public HealthContributor getContributor(String name) {
return indicators.get(name);
}
@Override
public Iterator> iterator() {
return indicators.entrySet().stream()
.map(e -> NamedContributor.of(e.getKey(), e.getValue()))
.iterator();
}
}
```
L'endpoint `/actuator/health` aggrega tutti gli indicatori. La configurazione `management.endpoint.health.show-details=always` espone i dettagli.
## Concetti avanzati
### 26. Come implementare la cache con Spring Cache e Redis?
L'astrazione Spring Cache semplifica l'integrazione di cache distribuite come Redis.
```java
// CacheConfig.java
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// Specific configurations per cache
Map cacheConfigs = Map.of(
"users", config.entryTtl(Duration.ofHours(1)),
"products", config.entryTtl(Duration.ofMinutes(30)),
"sessions", config.entryTtl(Duration.ofMinutes(5))
);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.withInitialCacheConfigurations(cacheConfigs)
.build();
}
}
```
```java
// ProductService.java
@Service
public class ProductService {
// Automatic result caching
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) {
log.info("Fetching product {} from database", id);
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
// Cache update on modification
@CachePut(value = "products", key = "#product.id")
public Product update(Product product) {
return productRepository.save(product);
}
// Cache invalidation
@CacheEvict(value = "products", key = "#id")
public void delete(Long id) {
productRepository.deleteById(id);
}
// Clear entire cache
@CacheEvict(value = "products", allEntries = true)
public void clearProductCache() {
log.info("Product cache cleared");
}
// Composite key for searches
@Cacheable(value = "productSearch", key = "#category + '-' + #minPrice + '-' + #maxPrice")
public List search(String category, BigDecimal minPrice, BigDecimal maxPrice) {
return productRepository.findByCategoryAndPriceBetween(category, minPrice, maxPrice);
}
}
```
Le annotazioni di cache sono trasparenti: il codice di business resta invariato e la cache viene gestita in modo dichiarativo.
### 27. Come implementare un task pianificato con @Scheduled?
Spring offre diversi modi per pianificare task ricorrenti.
```java
// SchedulerConfig.java
@Configuration
@EnableScheduling
public class SchedulerConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
scheduler.setThreadNamePrefix("scheduled-");
scheduler.setErrorHandler(t -> log.error("Scheduled task error", t));
return scheduler;
}
}
```
```java
// ScheduledTasks.java
@Component
public class ScheduledTasks {
private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
// Execute every 5 minutes
@Scheduled(fixedRate = 5 * 60 * 1000)
public void syncExternalData() {
log.info("Starting external data sync");
// Sync with external system
}
// Execute 10 seconds after the previous one ends
@Scheduled(fixedDelay = 10000, initialDelay = 5000)
public void processQueue() {
// Queue processing with delay between executions
}
// Cron expression: every day at 2 AM
@Scheduled(cron = "0 0 2 * * *", zone = "Europe/Paris")
public void dailyCleanup() {
log.info("Running daily cleanup");
cleanupService.removeExpiredSessions();
cleanupService.archiveOldData();
}
// Configurable cron via properties
@Scheduled(cron = "${app.reports.cron:0 0 6 * * MON}")
public void generateWeeklyReport() {
reportService.generateAndSend();
}
}
```
```java
// ConditionalScheduling.java - Conditional activation
@Component
@ConditionalOnProperty(name = "app.scheduling.enabled", havingValue = "true")
public class ConditionalScheduledTasks {
@Scheduled(fixedRate = 60000)
public void monitorSystem() {
// Monitoring active only if configured
}
}
```
I task pianificati girano in un pool di thread separato per non bloccare l'elaborazione delle richieste.
### 28. Come gestire gli eventi con ApplicationEvent?
Il sistema degli eventi consente di disaccoppiare i componenti dell'applicazione.
```java
// UserRegisteredEvent.java
public class UserRegisteredEvent extends ApplicationEvent {
private final User user;
private final Instant registeredAt;
public UserRegisteredEvent(Object source, User user) {
super(source);
this.user = user;
this.registeredAt = Instant.now();
}
public User getUser() { return user; }
public Instant getRegisteredAt() { return registeredAt; }
}
```
```java
// UserService.java - Event publishing
@Service
public class UserService {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public User registerUser(CreateUserRequest request) {
User user = new User(request.email(), request.name());
user = userRepository.save(user);
// Publish event after transaction
eventPublisher.publishEvent(new UserRegisteredEvent(this, user));
return user;
}
}
```
```java
// UserEventListeners.java - Event listeners
@Component
public class UserEventListeners {
private static final Logger log = LoggerFactory.getLogger(UserEventListeners.class);
// Synchronous listener
@EventListener
public void handleUserRegistered(UserRegisteredEvent event) {
log.info("User registered: {}", event.getUser().getEmail());
}
// Asynchronous listener to avoid blocking the main thread
@Async
@EventListener
public void sendWelcomeEmail(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.getUser());
}
// Transactional listener: executes after commit
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void notifyExternalSystem(UserRegisteredEvent event) {
// External notification only if transaction succeeds
externalApiClient.notifyNewUser(event.getUser().getId());
}
// Conditional listener with SpEL
@EventListener(condition = "#event.user.role == 'PREMIUM'")
public void handlePremiumUserRegistered(UserRegisteredEvent event) {
premiumService.initializePremiumFeatures(event.getUser());
}
}
```
Gli eventi transazionali (`@TransactionalEventListener`) garantiscono coerenza tra database ed effetti collaterali.
### 29. Come implementare un client HTTP con RestClient (Spring Boot 3+)?
`RestClient` è l'API moderna e fluente per le chiamate HTTP sincrone, in sostituzione di `RestTemplate`.
```java
// ExternalApiClient.java
@Component
public class ExternalApiClient {
private final RestClient restClient;
public ExternalApiClient(RestClient.Builder builder, ExternalApiProperties props) {
this.restClient = builder
.baseUrl(props.getBaseUrl())
.defaultHeader("Authorization", "Bearer " + props.getApiKey())
.defaultHeader("Accept", "application/json")
.requestInterceptor((request, body, execution) -> {
log.debug("Request: {} {}", request.getMethod(), request.getURI());
return execution.execute(request, body);
})
.build();
}
public User fetchUser(Long id) {
return restClient.get()
.uri("/users/{id}", id)
.retrieve()
.body(User.class);
}
public List searchProducts(String query, int page) {
return restClient.get()
.uri(uriBuilder -> uriBuilder
.path("/products/search")
.queryParam("q", query)
.queryParam("page", page)
.build())
.retrieve()
.body(new ParameterizedTypeReference>() {});
}
public Order createOrder(CreateOrderRequest request) {
return restClient.post()
.uri("/orders")
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
throw new OrderValidationException("Invalid order: " + res.getStatusCode());
})
.body(Order.class);
}
// Error handling with ResponseEntity
public Optional findUser(Long id) {
ResponseEntity response = restClient.get()
.uri("/users/{id}", id)
.retrieve()
.toEntity(User.class);
return response.getStatusCode().is2xxSuccessful()
? Optional.ofNullable(response.getBody())
: Optional.empty();
}
}
```
`RestClient` offre un'API simile a `WebClient` ma per chiamate bloccanti, ideale per le applicazioni che non usano la programmazione reattiva.
### 30. Come gestire le migrazioni del database con Flyway?
Flyway automatizza le migrazioni di schema in modo versionato e riproducibile.
```properties
# application.properties
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=true
spring.flyway.validate-on-migrate=true
```
```sql
-- db/migration/V1__create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
```
```sql
-- db/migration/V2__add_user_status.sql
ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'ACTIVE';
CREATE INDEX idx_users_status ON users(status);
```
```sql
-- db/migration/V3__create_orders_table.sql
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
total_amount DECIMAL(10, 2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
```
```java
// FlywayConfig.java - Advanced configuration
@Configuration
public class FlywayConfig {
@Bean
public FlywayMigrationStrategy migrationStrategy() {
return flyway -> {
// Validation before migration
flyway.validate();
// Execute migrations
flyway.migrate();
};
}
// Callback for post-migration actions
@Bean
public Callback flywayCallback() {
return new BaseCallback() {
@Override
public void handle(Event event, Context context) {
if (event == Event.AFTER_MIGRATE) {
log.info("Migrations completed successfully");
}
}
};
}
}
```
Ogni file di migrazione viene eseguito una sola volta e il suo checksum viene verificato per individuare modifiche accidentali.
## Conclusione
Queste 30 domande coprono gli aspetti essenziali di Spring Boot valutati nei colloqui tecnici. Padroneggiare questi concetti dimostra una conoscenza approfondita del framework e delle buone pratiche dello sviluppo Java.
**Checklist di preparazione:**
- ✅ Comprendere il meccanismo dell'auto-configurazione e le sue condizioni
- ✅ Padroneggiare Spring Data JPA: query, transazioni, problema N+1
- ✅ Saper configurare Spring Security con JWT e sicurezza a livello di metodo
- ✅ Conoscere le diverse annotazioni di test e i loro casi d'uso
- ✅ Usare profili e @ConfigurationProperties per la configurazione
- ✅ Implementare cache, scheduling ed eventi
- ✅ Esporre metriche personalizzate e indicatori di salute
- ✅ Padroneggiare RestClient per le chiamate HTTP moderne
La pratica costante su progetti reali resta il modo migliore per consolidare queste conoscenze e rispondere alle domande tecniche con sicurezza.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/it/blog/spring-boot/30-spring-boot-interview-questions