Introduction
Getting Started
- QuickStart
Patterns
- Languages
- Supported Languages
- Python
- Java
- JavaScript
- TypeScript
- Node.js
- React
- Fastify
- Next.js
- Terraform
- C#
- C++
- C
- Go
- Rust
- Swift
- React Native
- Spring Boot
- Kotlin
- Flutter
- Ruby
- PHP
- Scala
- Perl
- R
- Dart
- Elixir
- Erlang
- Haskell
- Lua
- Julia
- Clojure
- Groovy
- Fortran
- COBOL
- Pascal
- Assembly
- Bash
- PowerShell
- SQL
- PL/SQL
- T-SQL
- MATLAB
- Objective-C
- VBA
- ABAP
- Apex
- Apache Camel
- Crystal
- D
- Delphi
- Elm
- F#
- Hack
- Lisp
- OCaml
- Prolog
- Racket
- Scheme
- Solidity
- Verilog
- VHDL
- Zig
- MongoDB
- ClickHouse
- MySQL
- GraphQL
- Redis
- Cassandra
- Elasticsearch
- Security
- Performance
Integrations
- Code Repositories
- Team Messengers
- Ticketing
Enterprise
Spring Boot is an open-source Java-based framework used to create microservices and standalone Spring applications with minimal configuration. It simplifies the bootstrapping and development of new Spring applications.
Spring Boot, while designed to simplify Java application development, can still be misused. Here are the most important anti-patterns to avoid when developing Spring Boot applications.
// Anti-pattern: Field injection
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private EmailService emailService;
// Methods using injected dependencies
}
// Better approach: Constructor injection
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
@Autowired // Optional in newer Spring versions
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
// Methods using injected dependencies
}
Field injection makes your code harder to test and hides dependencies. Use constructor injection to make dependencies explicit and enable easier unit testing.
// Anti-pattern: Hardcoded configuration values
@Service
public class EmailService {
private final String smtpServer = "smtp.example.com";
private final int port = 587;
private final String username = "user";
private final String password = "password";
// Service methods
}
// Better approach: Use @ConfigurationProperties
@Configuration
@ConfigurationProperties(prefix = "mail")
public class EmailProperties {
private String smtpServer;
private int port;
private String username;
private String password;
// Getters and setters
}
@Service
public class EmailService {
private final EmailProperties emailProperties;
public EmailService(EmailProperties emailProperties) {
this.emailProperties = emailProperties;
}
// Service methods using emailProperties
}
Hardcoded configuration values make your application inflexible and difficult to deploy in different environments. Use @ConfigurationProperties
to externalize configuration.
// Anti-pattern: Misusing @Transactional
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final EmailService emailService;
@Autowired
public OrderService(OrderRepository orderRepository, EmailService emailService) {
this.orderRepository = orderRepository;
this.emailService = emailService;
}
@Transactional
public void processOrder(Order order) {
orderRepository.save(order);
emailService.sendOrderConfirmation(order); // Might fail and roll back DB transaction
}
}
// Better approach: Separate transactional boundaries
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final EmailService emailService;
@Autowired
public OrderService(OrderRepository orderRepository, EmailService emailService) {
this.orderRepository = orderRepository;
this.emailService = emailService;
}
public void processOrder(Order order) {
saveOrder(order);
try {
emailService.sendOrderConfirmation(order);
} catch (Exception e) {
// Log error but don't roll back transaction
log.error("Failed to send confirmation email", e);
}
}
@Transactional
private void saveOrder(Order order) {
orderRepository.save(order);
}
}
Misusing @Transactional
can lead to unexpected rollbacks or transaction leaks. Be mindful of transaction boundaries and separate transactional operations from non-transactional ones.
<!-- Anti-pattern: Manually managing dependencies -->
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Many more dependencies -->
</dependencies>
<!-- Better approach: Use Spring Boot starters -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
Manually managing dependencies can lead to version conflicts and missing transitive dependencies. Use Spring Boot starters to get curated, compatible dependency sets.
// Anti-pattern: Manual JDBC or JPA code
@Repository
public class UserRepositoryImpl implements UserRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public User findById(Long id) {
return jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?",
new Object[]{id},
(rs, rowNum) -> new User(
rs.getLong("id"),
rs.getString("username"),
rs.getString("email")
)
);
}
// Many more manual implementations
}
// Better approach: Use Spring Data repositories
public interface UserRepository extends JpaRepository<User, Long> {
// All basic CRUD operations are automatically implemented
// Custom query methods
Optional<User> findByEmail(String email);
List<User> findByLastNameOrderByFirstNameAsc(String lastName);
@Query("SELECT u FROM User u WHERE u.status = :status")
List<User> findByStatus(@Param("status") UserStatus status);
}
Writing manual data access code is error-prone and time-consuming. Use Spring Data repositories to automatically implement common data access patterns.
// Anti-pattern: Poor exception handling
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id); // Might throw exception
}
}
// Better approach: Proper exception handling
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(user -> ResponseEntity.ok(user))
.orElseThrow(() -> new ResourceNotFoundException("User not found with id " + id));
}
}
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<?> handleResourceNotFoundException(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
// Other exception handlers
}
Not handling exceptions properly leads to poor user experience and security issues. Use @ControllerAdvice
and @ExceptionHandler
to handle exceptions globally.
// Anti-pattern: Environment-specific configuration in code
@Configuration
public class DatabaseConfig {
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
if (System.getProperty("env").equals("dev")) {
dataSource.setUrl("jdbc:h2:mem:testdb");
// Dev configuration
} else {
dataSource.setUrl("jdbc:mysql://production-db:3306/app");
// Production configuration
}
return dataSource;
}
}
// Better approach: Use Spring profiles
@Configuration
@Profile("dev")
public class DevDatabaseConfig {
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setUrl("jdbc:h2:mem:testdb");
// Dev configuration
return dataSource;
}
}
@Configuration
@Profile("prod")
public class ProdDatabaseConfig {
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setUrl("jdbc:mysql://production-db:3306/app");
// Production configuration
return dataSource;
}
}
Hardcoding environment-specific configuration makes deployment difficult. Use Spring profiles to separate configuration for different environments.
// Anti-pattern: Custom health checks and metrics
@RestController
@RequestMapping("/system")
public class SystemController {
@Autowired
private DataSource dataSource;
@GetMapping("/health")
public Map<String, Object> health() {
Map<String, Object> health = new HashMap<>();
try (Connection conn = dataSource.getConnection()) {
health.put("database", "UP");
} catch (SQLException e) {
health.put("database", "DOWN");
}
// More manual health checks
return health;
}
}
// Better approach: Use Spring Boot Actuator
// In pom.xml or build.gradle
// <dependency>
// <groupId>org.springframework.boot</groupId>
// <artifactId>spring-boot-starter-actuator</artifactId>
// </dependency>
// In application.properties or application.yml
// management.endpoints.web.exposure.include=health,info,metrics
// management.endpoint.health.show-details=always
// Custom health indicator if needed
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
// Check health logic
if (isHealthy()) {
return Health.up().withDetail("details", "Service is running smoothly").build();
}
return Health.down().withDetail("details", "Service is experiencing issues").build();
}
}
Implementing custom monitoring endpoints is unnecessary and lacks features. Use Spring Boot Actuator for production-ready features like health checks, metrics, and monitoring.
// Anti-pattern: Custom security implementation
@Component
public class CustomAuthFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String token = httpRequest.getHeader("Authorization");
// Manual token validation
if (token != null && validateToken(token)) {
chain.doFilter(request, response);
} else {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
private boolean validateToken(String token) {
// Custom token validation logic
return true;
}
}
// Better approach: Use Spring Security
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtTokenProvider tokenProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
.and()
.apply(new JwtConfigurer(tokenProvider));
}
}
Custom security implementations are prone to vulnerabilities. Use Spring Security for robust, well-tested security features.
// Anti-pattern: Manual test setup
public class UserServiceTest {
private UserRepository userRepository;
private EmailService emailService;
private UserService userService;
@Before
public void setup() {
userRepository = mock(UserRepository.class);
emailService = mock(EmailService.class);
userService = new UserService(userRepository, emailService);
}
@Test
public void testFindById() {
// Test implementation
}
}
// Better approach: Use Spring Boot Test
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceIntegrationTest {
@Autowired
private UserService userService;
@MockBean
private EmailService emailService;
@Test
public void testFindById() {
// Test implementation with Spring Boot test features
}
}
// Or for slices of the application
@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
public void testGetUser() throws Exception {
// Test with MockMvc
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1));
}
}
Manual test setup is verbose and error-prone. Use Spring Boot’s testing features like @SpringBootTest
, @WebMvcTest
, and @DataJpaTest
for more effective testing.
<!-- Add to your pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
# application.properties for development
spring.devtools.restart.enabled=true
spring.devtools.livereload.enabled=true
Not using Spring Boot DevTools during development leads to slower development cycles. DevTools provides automatic restart, live reload, and other development-time features that improve productivity.
// Anti-pattern: Manual caching
@Service
public class ProductService {
private final ProductRepository productRepository;
private final Map<Long, Product> productCache = new ConcurrentHashMap<>();
@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Product getProductById(Long id) {
if (productCache.containsKey(id)) {
return productCache.get(id);
}
Product product = productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
productCache.put(id, product);
return product;
}
public void updateProduct(Product product) {
productRepository.save(product);
productCache.put(product.getId(), product);
}
}
// Better approach: Use Spring Boot Caching
@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("products");
}
}
@Service
public class ProductService {
private final ProductRepository productRepository;
@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Cacheable("products")
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
}
@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
return productRepository.save(product);
}
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
}
Manual caching is error-prone and difficult to maintain. Use Spring Boot’s caching abstraction with annotations like @Cacheable
, @CachePut
, and @CacheEvict
for declarative caching.
// Anti-pattern: Manual validation
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<User> createUser(@RequestBody UserDto userDto) {
// Manual validation
if (userDto.getEmail() == null || !userDto.getEmail().contains("@")) {
throw new BadRequestException("Invalid email");
}
if (userDto.getPassword() == null || userDto.getPassword().length() < 8) {
throw new BadRequestException("Password must be at least 8 characters");
}
// More validations...
// Process valid user
User user = userService.createUser(userDto);
return ResponseEntity.ok(user);
}
}
// Better approach: Use Bean Validation
public class UserDto {
@NotBlank(message = "Name is required")
private String name;
@Email(message = "Invalid email format")
@NotBlank(message = "Email is required")
private String email;
@Size(min = 8, message = "Password must be at least 8 characters")
private String password;
// Getters and setters
}
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody UserDto userDto) {
// Validation is handled automatically
User user = userService.createUser(userDto);
return ResponseEntity.ok(user);
}
}
// Global validation error handler
@ControllerAdvice
public class ValidationExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.badRequest().body(errors);
}
}
Manual validation is error-prone and verbose. Use Bean Validation (JSR-380) with annotations like @Valid
, @NotNull
, @Size
, etc., for declarative validation.
// Anti-pattern: Custom health check endpoints
@RestController
@RequestMapping("/health")
public class HealthCheckController {
@Autowired
private DataSource dataSource;
@GetMapping
public Map<String, Object> checkHealth() {
Map<String, Object> health = new HashMap<>();
health.put("status", "UP");
try (Connection conn = dataSource.getConnection()) {
health.put("database", "UP");
} catch (Exception e) {
health.put("database", "DOWN");
health.put("status", "DOWN");
}
// More health checks
return health;
}
}
// Better approach: Use Spring Boot Actuator
// In application.properties
// management.endpoint.health.show-details=always
// management.endpoints.web.exposure.include=health,info,metrics
// Custom health indicator
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private final DataSource dataSource;
@Autowired
public DatabaseHealthIndicator(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Health health() {
try (Connection conn = dataSource.getConnection()) {
// Execute a simple query
try (Statement stmt = conn.createStatement()) {
stmt.execute("SELECT 1");
}
return Health.up().withDetail("database", "Available").build();
} catch (Exception e) {
return Health.down()
.withDetail("database", "Unavailable")
.withException(e)
.build();
}
}
}
Custom health check endpoints lack standardization and features. Use Spring Boot Actuator’s health endpoints and custom health indicators for comprehensive health monitoring.