Essential patterns, troubleshooting, and best practices for Spring Boot applications
One of the most common startup failures involves database connectivity:
# application.properties - MySQL example spring.datasource.url=jdbc:mysql://localhost:3306/myapp spring.datasource.username=root spring.datasource.password=yourpassword spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # JPA configuration spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
// โ Wrong: Missing annotation public class UserService { // This won't be managed by Spring } // โ Correct: Proper annotation @Service public class UserService { @Autowired private UserRepository userRepository; }
src/main/java/com/company/myapp/ โโโ MyAppApplication.java // Main class โโโ config/ // Configuration classes โ โโโ SecurityConfig.java โ โโโ DatabaseConfig.java โโโ controller/ // REST controllers โ โโโ UserController.java โ โโโ ProductController.java โโโ service/ // Business logic โ โโโ UserService.java โ โโโ ProductService.java โโโ repository/ // Data access layer โ โโโ UserRepository.java โ โโโ ProductRepository.java โโโ model/ // Entity classes โ โโโ User.java โ โโโ Product.java โโโ dto/ // Data transfer objects โ โโโ UserDto.java โ โโโ ProductDto.java โโโ exception/ // Custom exceptions โโโ GlobalExceptionHandler.java
Handle HTTP requests and responses. Keep controllers thin.
@RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @GetMapping public ResponseEntity<List<UserDto>> getAllUsers() { return ResponseEntity.ok(userService.getAllUsers()); } @PostMapping public ResponseEntity<UserDto> createUser(@RequestBody CreateUserRequest request) { UserDto created = userService.createUser(request); return ResponseEntity.status(HttpStatus.CREATED).body(created); } }
Contains business logic and coordinates between layers.
@Service @Transactional public class UserService { @Autowired private UserRepository userRepository; public UserDto createUser(CreateUserRequest request) { User user = new User(request.getName(), request.getEmail()); User saved = userRepository.save(user); return UserMapper.toDto(saved); } public List<UserDto> getAllUsers() { return userRepository.findAll() .stream() .map(UserMapper::toDto) .collect(Collectors.toList()); } }
Data access and persistence operations.
@Repository public interface UserRepository extends JpaRepository<User, Long> { List<User> findByEmailContaining(String email); @Query("SELECT u FROM User u WHERE u.active = true") List<User> findActiveUsers(); Optional<User> findByEmail(String email); boolean existsByEmail(String email); }
@Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 100) private String name; @Column(unique = true, nullable = false) private String email; @CreationTimestamp private LocalDateTime createdAt; @UpdateTimestamp private LocalDateTime updatedAt; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<Order> orders = new ArrayList<>(); // Constructors, getters, setters... }
Manage database schema changes systematically:
-- src/main/resources/db/migration/V1__Create_users_table.sql CREATE TABLE users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**").permitAll() .requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .csrf(csrf -> csrf.disable()); return http.build(); } }
@Service public class JwtService { private final String SECRET_KEY = "your-secret-key"; private final long EXPIRATION_TIME = 86400000; // 24 hours public String generateToken(UserDetails userDetails) { return Jwts.builder() .setSubject(userDetails.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public Boolean validateToken(String token, UserDetails userDetails) { final String username = extractUsername(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } }
Test individual components in isolation.
@ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; @Test void createUser_ShouldReturnUserDto() { // Given CreateUserRequest request = new CreateUserRequest("John", "[email protected]"); User savedUser = new User("John", "[email protected]"); when(userRepository.save(any(User.class))).thenReturn(savedUser); // When UserDto result = userService.createUser(request); // Then assertThat(result.getName()).isEqualTo("John"); verify(userRepository).save(any(User.class)); } }
Test complete request-response cycles.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) class UserControllerIntegrationTest { @Autowired private TestRestTemplate restTemplate; @Autowired private UserRepository userRepository; @Test void createUser_ShouldReturnCreatedUser() { // Given CreateUserRequest request = new CreateUserRequest("Jane", "[email protected]"); // When ResponseEntity<UserDto> response = restTemplate.postForEntity( "/api/users", request, UserDto.class ); // Then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(response.getBody().getEmail()).isEqualTo("[email protected]"); } }
Test data access layer with @DataJpaTest.
@DataJpaTest class UserRepositoryTest { @Autowired private TestEntityManager entityManager; @Autowired private UserRepository userRepository; @Test void findByEmail_ShouldReturnUser() { // Given User user = new User("Test", "[email protected]"); entityManager.persistAndFlush(user); // When Optional<User> found = userRepository.findByEmail("[email protected]"); // Then assertThat(found).isPresent(); assertThat(found.get().getName()).isEqualTo("Test"); } }
# HikariCP configuration
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.connection-timeout=20000
# Enable Actuator endpoints management.endpoints.web.exposure.include=health,info,metrics,prometheus management.endpoint.health.show-details=always management.metrics.export.prometheus.enabled=true
Key Metrics to Monitor: