Anti Patterns
Why Anti-Patterns Matter
Spring Boot’s “convention over configuration” approach enables rapid development but introduces new failure modes when developers misunderstand auto-configuration, abuse starter dependencies, or misconfigure profiles. Recognizing these anti-patterns prevents production incidents, debugging nightmares, and performance degradation.
Over-Reliance on Auto-Configuration
Anti-Pattern: Treating Auto-Configuration as Magic
Problem:
@SpringBootApplication // => Developer assumes "it just works" without understanding what Boot configures
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
// => No understanding of:
// => - Which beans Boot creates automatically
// => - When auto-configuration triggers
// => - How to override defaults
}
}
// => Application runs in development, fails in production
// => Reason: Different classpath, different auto-configuration decisionsProduction Failure Scenario:
Development: H2 database on classpath → Boot auto-configures H2 DataSource
Production: PostgreSQL driver added → Boot auto-configures PostgreSQL, but no connection properties set
Result: Application starts but fails on first database accessSolution: Understand Auto-Configuration Conditions
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// => Enable debug logging to see auto-configuration decisions
SpringApplication app = new SpringApplication(Application.class);
app.setLogStartupInfo(true); // => Logs all auto-configured beans
app.run(args);
}
// => Explicitly configure critical beans (don't rely solely on auto-config)
@Bean
@ConditionalOnMissingBean // => Only create if Boot didn't auto-configure
public DataSource dataSource() {
// => Explicit configuration: clear what happens in all environments
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost/zakat");
config.setUsername("zakat_user");
config.setPassword("password");
return new HikariDataSource(config);
}
}application.properties:
# => Enable auto-configuration report
debug=true
# => Shows:
# => - Positive matches: auto-configurations that applied
# => - Negative matches: auto-configurations that didn't apply (with reasons)
# => - Exclusions: auto-configurations explicitly excludedBest Practice:
- Run with
debug=truein development to understand Boot’s decisions - Explicitly configure production-critical beans (database, security)
- Document assumptions about classpath dependencies
@SpringBootApplication Misuse
Anti-Pattern: Placing @SpringBootApplication in Root Package
Problem:
// File: src/main/java/Application.java (root package!)
@SpringBootApplication // => Scans ENTIRE classpath (root package = all packages)
// => Includes third-party libraries if in root
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
// => Slow startup: scanning thousands of classes
// => Potential conflicts: third-party @Component classes registered as beans
}
}Consequence:
Startup time: 45 seconds (scanning 10,000+ classes)
Memory usage: 500MB (unnecessary bean registrations)
Conflicts: Third-party library @Component classes registered as beansSolution: Use Package Structure
// File: src/main/java/com/example/zakat/Application.java
package com.example.zakat; // => Clear package boundary
@SpringBootApplication // => Scans com.example.zakat and subpackages only
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// Project structure:
// com.example.zakat (Application.java here)
// ├── controller (@RestController classes)
// ├── service (@Service classes)
// ├── repository (@Repository classes)
// └── config (@Configuration classes)Result:
Startup time: 8 seconds (scanning 150 relevant classes)
Memory usage: 120MB (only necessary beans)
No conflicts: Third-party libraries not scannedAnti-Pattern: Multiple @SpringBootApplication Classes
Problem:
// File: com/example/zakat/ZakatApplication.java
@SpringBootApplication
public class ZakatApplication {
public static void main(String[] args) {
SpringApplication.run(ZakatApplication.class, args);
}
}
// File: com/example/zakat/TestApplication.java
@SpringBootApplication // => SECOND @SpringBootApplication in same package hierarchy
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
// => Both applications scan same packages
// => Unpredictable component registration
// => Tests may load wrong application contextSolution: One Main Application, Test Configurations
// Main application
@SpringBootApplication
public class ZakatApplication {
public static void main(String[] args) {
SpringApplication.run(ZakatApplication.class, args);
}
}
// Test configuration (NOT @SpringBootApplication)
@TestConfiguration // => Test-specific beans only
public class TestConfig {
@Bean
@Primary // => Overrides production bean for tests
public DataSource testDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build(); // => In-memory database for tests
}
}
// Test class
@SpringBootTest // => Loads ZakatApplication context
@Import(TestConfig.class) // => Adds test-specific beans
class ZakatServiceTest {
// => Uses main application configuration + test overrides
}Disabling Auto-Configuration Incorrectly
Anti-Pattern: Excluding Too Much
Problem:
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class, // => Excludes DataSource
HibernateJpaAutoConfiguration.class, // => Excludes JPA
TransactionAutoConfiguration.class, // => Excludes transactions
JdbcTemplateAutoConfiguration.class // => Excludes JDBC template
}) // => Must now manually configure ALL these components
public class Application {
// => Developer wanted to customize DataSource
// => Accidentally disabled ALL data access auto-configuration
}Consequence:
@Service
public class ZakatService {
@Autowired
private JdbcTemplate jdbcTemplate; // => Bean not found!
// => JdbcTemplateAutoConfiguration excluded
}
// Error: No qualifying bean of type 'JdbcTemplate' availableSolution: Exclude Minimally, Override Specifically
@SpringBootApplication // => Let Boot auto-configure MOST things
public class Application {
// => Override specific bean instead of excluding entire auto-configuration
@Bean
@Primary // => Takes precedence over Boot's auto-configured DataSource
public DataSource dataSource() {
// => Custom DataSource: Boot's auto-configuration backs off
// => Other data access components still auto-configured
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost/zakat");
config.setMaximumPoolSize(50); // => Custom pool size
return new HikariDataSource(config);
}
// => JdbcTemplate, TransactionManager, etc. still auto-configured
// => Use custom DataSource automatically
}When to Exclude:
- Multiple data sources: Exclude
DataSourceAutoConfiguration, manually configure all - Custom persistence: Using MyBatis instead of JPA → exclude JPA auto-config
- No database: Exclude all data-related auto-configs
Anti-Pattern: Fighting Auto-Configuration
Problem:
@SpringBootApplication(exclude = WebMvcAutoConfiguration.class) // => Disabled Web MVC
public class Application {
@Bean // => Manually recreating what Boot already provides
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
return new RequestMappingHandlerMapping();
}
@Bean // => Recreating Jackson configuration
public MappingJackson2HttpMessageConverter messageConverter() {
return new MappingJackson2HttpMessageConverter();
}
@Bean // => Recreating view resolver
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
// => 50+ more beans to manually configure...
}Solution: Customize via Properties
# application.yml: Configure Boot's auto-configured components
spring:
mvc:
view:
prefix: /WEB-INF/views/ # => Customizes view resolver
suffix: .jsp # => Boot's auto-configuration uses these values
jackson:
serialization:
indent-output: true # => Customizes Jackson (already auto-configured)
date-format: yyyy-MM-dd # => Date format
server:
servlet:
context-path: /api # => Context path for servlet containerPrinciple: Customize, Don’t Replace
Use Boot’s auto-configuration, customize via properties. Only exclude when fundamentally incompatible.
Property Configuration Anti-Patterns
Anti-Pattern: Hardcoding Environment-Specific Values
Problem:
@Configuration
public class DataConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://prod-db.example.com/zakat"); // => Hard-coded production URL
config.setUsername("prod_user"); // => Hard-coded credentials
config.setPassword("SecretPassword123"); // => SECURITY VIOLATION: password in code
config.setMaximumPoolSize(50); // => Hard-coded production pool size
return new HikariDataSource(config);
}
}
// => Cannot run in development (prod database required)
// => Cannot change password without recompiling
// => Password visible in Git historySolution: Externalize All Environment-Specific Config
# application.yml: No environment-specific values
spring:
datasource:
url: ${DB_URL} # => Environment variable
username: ${DB_USERNAME} # => Environment variable
password: ${DB_PASSWORD} # => Environment variable (secret)
hikari:
maximum-pool-size: ${DB_POOL_SIZE:10} # => Default 10 if not set
# application-prod.yml: Production defaults (still overridable)
spring:
datasource:
hikari:
maximum-pool-size: 50 # => Production default (can be overridden by env var)
# application-dev.yml: Development defaults
spring:
datasource:
url: jdbc:h2:mem:zakatdb # => Development: in-memory database
hikari:
maximum-pool-size: 5 # => Development: small poolSetting environment variables:
# Production environment
export DB_URL=jdbc:postgresql://prod-db.example.com/zakat
export DB_USERNAME=prod_user
export DB_PASSWORD=$(vault read -field=password secret/zakat/db) # From Vault
export DB_POOL_SIZE=50
export SPRING_PROFILES_ACTIVE=prod
java -jar zakat-service.jarAnti-Pattern: Duplicating Properties Across Profile Files
Problem:
# application-dev.yml
server:
port: 8080 # => Duplicated
spring:
application:
name: zakat-service # => Duplicated
datasource:
url: jdbc:h2:mem:zakatdb
logging:
level:
com.example.zakat: DEBUG
# application-staging.yml
server:
port: 8080 # => Duplicated
spring:
application:
name: zakat-service # => Duplicated
datasource:
url: jdbc:postgresql://staging-db/zakat
logging:
level:
com.example.zakat: INFO
# application-prod.yml
server:
port: 8080 # => Duplicated (3 times!)
spring:
application:
name: zakat-service # => Duplicated (3 times!)
datasource:
url: jdbc:postgresql://prod-db/zakat
logging:
level:
com.example.zakat: INFOSolution: Base Config + Profile Overrides
# application.yml: Shared across ALL profiles
server:
port: 8080 # => Default port (single source of truth)
spring:
application:
name: zakat-service # => Same in all environments
# application-dev.yml: ONLY dev-specific overrides
spring:
datasource:
url: jdbc:h2:mem:zakatdb # => Only what differs from base
logging:
level:
com.example.zakat: DEBUG # => Verbose logging for development
# application-prod.yml: ONLY prod-specific overrides
spring:
datasource:
url: ${DB_URL} # => Only what differs from base
logging:
level:
com.example.zakat: INFO # => Less verbose for productionAnti-Pattern: Ignoring Property Precedence
Problem:
# application.yml
spring:
datasource:
url: jdbc:h2:mem:zakatdb # => Developer expects this
# application-prod.yml
spring:
datasource:
url: jdbc:postgresql://prod-db/zakat # => Production override
# Command line
java -jar zakat-service.jar --spring.datasource.url=jdbc:postgresql://staging-db/zakat
# Developer confused: "Why isn't it using application-prod.yml?"
# => Answer: Command-line arguments override property filesSolution: Understand Property Precedence (Highest to Lowest)
- Command-line arguments:
--spring.datasource.url=... - Java system properties:
-Dspring.datasource.url=... - OS environment variables:
SPRING_DATASOURCE_URL=... - Profile-specific properties:
application-{profile}.yml - Base properties:
application.yml
Use precedence intentionally:
# application-prod.yml: Production defaults
spring:
datasource:
url: jdbc:postgresql://prod-db/zakat # => Default for prod profile
hikari:
maximum-pool-size: 50
# Override at runtime for specific scenarios:
java -jar zakat-service.jar \
--spring.profiles.active=prod \
--spring.datasource.url=jdbc:postgresql://prod-db-replica/zakat # => Override defaultStarter Dependency Anti-Patterns
Anti-Pattern: Including Conflicting Starters
Problem:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- => Includes Tomcat embedded server -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<!-- => Includes Reactor Netty server -->
<!-- => CONFLICT: Cannot run both servlet (Tomcat) and reactive (Netty) -->
</dependency>
</dependencies>Consequence:
Application startup failure:
Both 'webflux' and 'web' starters detected
Cannot run both servlet and reactive stacks simultaneouslySolution: Choose One Web Stack
<!-- Servlet stack (Spring MVC + Tomcat) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- OR -->
<!-- Reactive stack (WebFlux + Netty) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- NOT both! -->Anti-Pattern: Not Excluding Transitive Dependencies
Problem:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<!-- => Includes Logback (Boot's default) -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<!-- => Includes Log4j2 -->
<!-- => CONFLICT: Both logging frameworks on classpath -->
</dependency>Consequence:
Multiple SLF4J bindings detected:
- Logback (from spring-boot-starter-logging)
- Log4j2 (from spring-boot-starter-log4j2)
Result: Unpredictable logging behaviorSolution: Exclude Default Logging
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!-- => Exclude Boot's default Logback dependency -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<!-- => Now Log4j2 is ONLY logging framework -->
</dependency>See Also
- Spring Boot Best Practices - Correct patterns
- Spring Anti-Patterns - Foundation anti-patterns
- Spring Boot Auto-Configuration Reference - Understanding auto-config
- Spring Boot Properties Reference - All configuration properties