The main selling point of Spring Boot is that it forces you to configure almost nothing by hand. Add a single dependency for working with a database, and JdbcTemplate is already there. Add a dependency for the web, and the application brings up an embedded server on its own. Let's work through, from scratch, how this is put together, where the application gets its settings from, and how to keep different settings for development and for the production environment.
Starters — bundles of dependencies in a single line
In the past, to bring in, say, the web layer, you had to list a dozen libraries by hand: Spring MVC itself, the server, a JSON library, validation — and make sure their versions were compatible. One incompatible version and the application would fail at startup with a cryptic error. That's slow and fragile.
A starter is a ready-made bundle of dependencies for a single task, assembled for you. Add one line and get everything you need in mutually consistent versions.
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web") // the whole web layer
implementation("org.springframework.boot:spring-boot-starter-data-jpa") // working with a database
}
Starters are easy to recognize by their name: they begin with spring-boot-starter-. A starter contains almost no code of its own — it's just a list of other libraries. Configuring them into a working state is the job of the next part.
How Spring Boot configures beans on its own
You add the database starter, and out of nowhere a ready-to-use DataSource and JdbcTemplate appear in the application, even though you never created them. In the past, every such object had to be assembled by hand: read the database address, username, and password, create a connection pool, wire everything together. Dozens of lines of the same boilerplate in every project.
Auto-configuration is the mechanism Spring Boot uses to do this setup for you. The idea is simple: "look at which libraries are on the project's classpath and configure them with sensible defaults."
It's all turned on by a single annotation on the main class:
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
@SpringBootApplication is three annotations rolled into one: marking the class as configuration, scanning your packages for @Component and @Service, and @EnableAutoConfiguration — that very instruction "configure everything you find in the project."
Inside the starters live pre-written configuration classes. At startup Spring Boot goes through them and decides, for each one, whether to apply it or not.
Conditional annotations — configure "if present"
How does Spring Boot know what exactly to configure? After all, one project has a database and another doesn't, and setting up a database in the latter would only get in the way.
The answer is conditional annotations. Each configuration class is marked with conditions, and it's applied only if those conditions are met.
@AutoConfiguration
@ConditionalOnClass(DataSource.class) // only if the DataSource class is on the classpath
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean // only if you didn't create your own DataSource
@ConditionalOnProperty(name = "spring.datasource.url") // only if the database address is set
public DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}
The most common conditions and what they mean:
@ConditionalOnClass— apply only if the required class is on the classpath. No database library — the database setup is simply skipped.@ConditionalOnMissingBean— apply only if no such bean exists yet. This means your configuration always wins: declare your ownDataSourceand Spring Boot steps aside, leaving yours in place.@ConditionalOnProperty— apply only if a certain value is set in the configuration. Handy for turning something on and off with a single line.
The upshot: you drop dependencies into the project, and Spring Boot assembles a working context out of them, without interfering where you've set something yourself.
Why a bean wasn't created
When "a bean should have appeared but it isn't there," there's no need to guess. Turn on debug mode:
debug=true
At startup the logs will show a report of the decisions made: what matched (satisfied conditions) and what was rejected and why. This is the first place to look.
Settings outside the code
The database address, passwords, and port number must not be hard-coded: development needs one value, the production server another, and passwords should never sit in the repository at all. Changing the code and rebuilding the application just to change a port is a bad approach.
Spring Boot lets you keep all such values outside — in a settings file or in environment variables. The main file is application.yml (or application.properties) in the project:
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://localhost:5432/shop
The same value can be set in several places, and they have a priority order. You don't need to memorize the whole list; the main rule is enough: the "closer" to launch a value is set, the more it wins. A command-line argument overrides an environment variable, which in turn overrides the application.yml file.
java -jar app.jar --server.port=9000 # overrides the port from application.yml
In practice it usually goes like this: common settings live in application.yml in the repository; passwords and addresses for the production server come through environment variables; and personal local tweaks go into application-local.yml, which is kept out of the repository.
@ConfigurationProperties and @Value
Settings from a file need to be read into the code somehow. There are two ways.
@Value is suitable for a single value:
@Value("${app.timeout:5000}") // 5000 is the default value if the setting is empty
private long timeoutMs;
But when there are many related values, @Value has to be repeated on every field, and it's easy to get a name wrong.
@ConfigurationProperties binds a whole group of settings to an object — in one go and with type checking:
app:
retries:
max-attempts: 3
backoff-ms: 1000
timeout-ms: 5000
@ConfigurationProperties(prefix = "app.retries")
public record RetryProperties(int maxAttempts, long backoffMs, long timeoutMs) {}
Now RetryProperties can be injected like any other dependency, with all three values already in place. Names in YAML are written with hyphens (max-attempts), and in the code as ordinary fields (maxAttempts); Spring matches one to the other itself.
A simple rule: one or two values — @Value; a group of related settings — @ConfigurationProperties.
Validating settings at startup
You can add rules to a group of settings, and then Spring will check them at startup:
@ConfigurationProperties(prefix = "app.retries")
@Validated
public record RetryProperties(
@Min(1) @Max(10) int maxAttempts,
@Positive long backoffMs,
@Positive long timeoutMs
) {}
If someone sets max-attempts: 0, the application won't start and will immediately say what's wrong. That's better than catching the error later, when the application is already running and fails in some unexpected place.
Profiles — different settings for different environments
For development you want to send emails "for pretend" and write detailed logs. On the production server — send real emails and log sparingly. Keeping separate builds of the application for this is inconvenient and dangerous.
A profile is a mode marker. A bean or a setting can be tied to a profile, and it will take effect only when that profile is active.
@Service
@Profile("prod")
public class RealEmailService implements EmailService { } // production only
@Service
@Profile({"dev", "test"})
public class FakeEmailService implements EmailService { } // in development and tests
You can activate a profile with the spring.profiles.active setting:
spring:
profiles:
active: prod
Or via the SPRING_PROFILES_ACTIVE=prod environment variable, or the --spring.profiles.active=prod launch argument. You can activate several at once, separated by commas.
Profiles also have their own separate settings files. The application-prod.yml file is layered on top of the common application.yml when the prod profile is active. This makes it convenient to keep common values in one place and the differences between environments in per-profile files: application-dev.yml, application-prod.yml.
Your own auto-configuration in a library
Suppose you have a reusable piece of infrastructure — a metrics wrapper or a shared set of filters — and you want to plug it into different projects with a single dependency, without copying settings around. Then it's worth packaging it as your own starter: it will configure itself in whoever plugs it in.
The principle is exactly the same as for the built-in starters: a configuration class with conditional annotations plus one service file that tells Spring Boot "here's my configuration class, apply it."
@AutoConfiguration
@ConditionalOnClass(MyService.class)
@ConditionalOnMissingBean(MyService.class)
public class MyAutoConfiguration {
@Bean
public MyService myService() {
return new MyService();
}
}
The pointer file goes into src/main/resources at the fixed path META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, and it contains just the name of your class:
com.example.mystarter.MyAutoConfiguration
After that, anyone who adds your library gets MyService in their context automatically — and can just as easily override it with their own bean thanks to @ConditionalOnMissingBean. This is exactly how the usecase-pattern library works, for example: one dependency, and the beans you need are already in place.
In short
- A starter is a ready-made bundle of dependencies for a single task in mutually consistent versions; recognizable by the
spring-boot-starter-*name. - Auto-configuration is the mechanism that configures beans on its own by looking at the libraries on the classpath; it's turned on through
@SpringBootApplication. - Conditional annotations decide whether to apply a configuration:
@ConditionalOnClass(is the class present),@ConditionalOnMissingBean(is there already a bean),@ConditionalOnProperty(is a value set). - Your own beans always win over the automatic ones —
@ConditionalOnMissingBeanis what makes this so. - A bean wasn't created — set
debug=trueand look at the report of the decisions made at startup. - Settings are kept outside the code (
application.yml, environment variables); the closer a value is to launch, the more it wins. @Value— for a single value;@ConfigurationProperties— for a group of related settings with type checking.@Validatedon a group of settings brings the application down at startup on an invalid value — the error is visible right away.- Profiles provide different settings for development and production;
application-<profile>.ymlis layered on top of the common file.
What to read next
- DI/IoC, bean lifecycle, scopes — how Spring creates the beans that auto-configuration is built on.
- Spring AOP —
@EnableAsync,@EnableCaching,@EnableTransactionManagementare also turned on through auto-configuration. - Spring Testing — how to check
@ConfigurationPropertiesand profiles in tests. - The usecase-pattern library — an example of a real starter with its own auto-configuration.