Spring Boot Core Concepts Tutorial
What is Spring Boot?
Spring Boot is an opinionated framework built on top of the Spring Framework. It helps developers create production-ready Java applications quickly by choosing sensible defaults and reducing the amount of configuration they must write.
Opinionated does not mean that Spring Boot removes your choices. It means Spring Boot provides a good default first, while still allowing you to change the configuration when your application needs something different.
Why was Spring Boot created?
Before Spring Boot, starting a Spring application often meant configuring many separate pieces manually:
web.xmland theDispatcherServlet- A servlet container such as Tomcat
- Maven or Gradle dependencies and compatible versions
- The Spring application context and bean configuration
- Property files and environment-specific settings
This setup could require hundreds of lines of configuration before the first business feature was written.
Minimal Spring Boot application
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
With this class and the right starter dependency, Spring Boot can start the application context and an embedded web server. You can run it like a normal Java program, without separately installing and deploying a WAR file to Tomcat.
Key advantages
| Advantage | What it means |
|---|---|
| Embedded Tomcat | The server can run inside your executable JAR. |
| Auto-configuration | Spring Boot configures common components when their dependencies are present. |
| Starter dependencies | One dependency brings a compatible group of libraries. |
| Less XML | Java configuration, annotations, and properties replace most manual XML setup. |
| Production features | Health checks, metrics, external configuration, and logging support are available. |
| Easy deployment | Build one JAR and run it with java -jar. |
Real-world analogy: opening a restaurant
Imagine opening a restaurant.
Spring Boot prepares the common infrastructure. You still decide what your application does, how its business rules work, and which features your users receive.
Spring vs Spring Boot
Spring is a large ecosystem for building Java applications. Traditionally, developers configured many objects and framework integrations by hand. Spring Boot adds conventions, starter dependencies, embedded servers, and automatic configuration to reduce that setup work.
Minimal application
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@SpringBootApplication combines component scanning,
auto-configuration, and a configuration class.
SpringApplication.run() creates the application context and
starts the embedded server when web dependencies are present.
Key ideas
- Convention over configuration: common choices work with little code.
- Starters: dependency bundles for web, data, security, and testing.
- Embedded server: run the app as a normal Java process or JAR.
- External configuration: change environment-specific values without recompiling.
Auto-Configuration
Auto-configuration is Spring Boot's way of choosing common beans for you. Boot checks the libraries on the classpath, your configuration properties, and the beans you already defined. It creates sensible defaults only when they are needed.
For example, when the web starter is present, Boot can configure an embedded server, Spring MVC infrastructure, JSON support, and a dispatcher servlet. If you define your own bean, Boot usually backs away so your explicit choice wins.
Including dependencies to activate auto-configuration
Auto-configuration is driven by the dependencies in your project. Adding a starter places the required libraries on the classpath, so Spring Boot can detect the feature and configure it.
<!-- Adds Spring MVC, Jackson, validation support, and embedded Tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Adds JPA, Hibernate, transactions, and repository support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
After these dependencies are available, Boot checks their classes and creates the matching infrastructure. A dependency does not automatically create a useful application feature by itself; your controllers, entities, repositories, and properties still need to be provided.
Excluding an auto-configuration class
Sometimes a dependency is present but your application does not need the feature it normally activates. You can exclude the related auto-configuration class.
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
The same exclusion can be configured without changing the Java class:
# application.properties
spring.autoconfigure.exclude=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
This is useful for a service that includes a database-related library transitively but does not connect to a database. Exclude only what you understand; removing required auto-configuration can cause missing beans or startup errors.
Excluding an individual dependency
Dependency exclusion is different from auto-configuration exclusion. A dependency exclusion prevents a library from being downloaded or used, while an auto-configuration exclusion keeps the library but prevents Boot from creating its default beans.
<!-- Keep Spring MVC, but remove embedded Tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
Replacing embedded Tomcat with Jetty
Spring Boot's web starter uses embedded Tomcat by default. To use Jetty instead, exclude the Tomcat starter and add the Jetty starter. Do not keep both servlet containers in the same application because the server configuration can become ambiguous.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Add the alternative embedded servlet container -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
Your controllers and REST endpoints usually do not change because Spring MVC sits above the servlet container. The main difference is the embedded server that receives HTTP requests and starts the application.
Useful auto-configuration controls
| Requirement | Approach | Example |
|---|---|---|
| Add a feature | Add a starter or library | spring-boot-starter-data-jpa |
| Change a default value | Set a property | server.port=8081 |
| Replace a default bean | Define your own @Bean |
Custom ObjectMapper |
| Disable configuration |
Use exclude or
spring.autoconfigure.exclude
|
DataSourceAutoConfiguration |
| Change the web server | Exclude Tomcat and add Jetty | spring-boot-starter-jetty |
Starters
A Spring Boot Starter is a carefully selected group of dependencies for one application capability. Instead of finding and adding every library separately, you add one starter and Spring Boot brings in the commonly required libraries.
spring-boot-starter-webExample: adding the Web starter
To build REST APIs, add the Web starter to pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
This starter commonly provides Spring MVC, an embedded servlet
container, JSON serialization through Jackson, validation support,
logging, and the Spring libraries needed to run a web application. You
can then write a controller without manually configuring a
DispatcherServlet or servlet container.
@RestController
@RequestMapping("/hello")
class HelloController {
@GetMapping
String hello() {
return "Hello Spring Boot";
}
}
Popular Spring Boot starters
| Starter | Use it for | What it usually provides |
|---|---|---|
spring-boot-starter |
Core applications | Core Spring Boot support, logging, and configuration |
spring-boot-starter-web |
REST APIs and MVC applications | Spring MVC, Jackson, validation, and embedded Tomcat |
spring-boot-starter-webflux |
Reactive web applications | WebFlux and a reactive HTTP stack |
spring-boot-starter-data-jpa |
Relational database access | Spring Data JPA, Hibernate, transactions, and repositories |
spring-boot-starter-jdbc |
SQL access with JDBC | JDBC template and connection-pool support |
spring-boot-starter-security |
Authentication and authorization | Security filters and secure defaults |
spring-boot-starter-validation |
Validating request objects | Jakarta Bean Validation integration |
spring-boot-starter-test |
Automated testing | JUnit, Mockito, AssertJ, and Spring Test |
spring-boot-starter-actuator |
Monitoring and operations | Health, metrics, and management endpoints |
spring-boot-starter-aop |
Aspect-oriented programming | Spring AOP and AspectJ integration support |
spring-boot-starter-cache |
Caching application results | Spring's cache abstraction and related support |
Starter dependencies and transitive dependencies
A starter may depend on several other libraries. These are called transitive dependencies. For example, the Web starter brings in the Spring Web modules and an embedded server without requiring you to list each one manually.
spring-boot-starter-web
├── spring-boot-starter
├── spring-web
├── spring-webmvc
├── spring-boot-starter-json
├── spring-boot-starter-tomcat
└── spring-boot-starter-validation
The exact dependency tree can vary with the Spring Boot version. Use Maven to inspect the actual libraries in your project:
mvn dependency:tree
Starters and auto-configuration are different
A starter provides libraries; auto-configuration uses those libraries to create default Beans and infrastructure.
Adding spring-boot-starter-data-jpa gives the project JPA
libraries. Auto-configuration then creates infrastructure such as an
EntityManagerFactory and transaction support when the
required database settings are available.
Dependency version management
When a Spring Boot parent or dependency management is used, Boot selects compatible versions for managed libraries. In most projects, do not add versions to individual Spring Boot starters.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>YOUR_SPRING_BOOT_VERSION</version>
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Specifying random versions for Spring libraries can create conflicts. Add a version only when you intentionally need a library version outside Boot's dependency management and have verified compatibility.
Starters with Gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Choosing the right starter
-
Use
spring-boot-starter-webfor traditional Servlet-based REST APIs. -
Use
spring-boot-starter-webfluxwhen your application is designed for reactive, non-blocking processing. -
Use
spring-boot-starter-data-jpafor object-relational mapping and repository-based SQL access. -
Use
spring-boot-starter-jdbcwhen you prefer SQL and JDBC templates instead of JPA. -
Use
spring-boot-starter-testin the test scope so test libraries are not packaged as production dependencies.
Adding a non-Spring library
Not every library has a Spring Boot starter. You can add a normal Maven
dependency and configure it yourself, or define a small
@Bean for the third-party client.
<dependency>
<groupId>com.example</groupId>
<artifactId>third-party-client</artifactId>
<version>1.0.0</version>
</dependency>
@Configuration
class ClientConfiguration {
@Bean
ThirdPartyClient thirdPartyClient() {
return new ThirdPartyClient();
}
}
Dependency Injection
Dependency injection (DI) means a class receives the objects it needs
instead of creating them with new. Spring owns these
objects as beans and connects them when the application context starts.
@Service
class GreetingService {
String message() { return "Hello from Spring"; }
}
@RestController
class GreetingController {
private final GreetingService service;
GreetingController(GreetingService service) {
this.service = service;
}
}
Constructor injection is preferred because the dependency is visible,
can be stored in a final field, and can be replaced with a
test double in a unit test.
Why dependency injection is useful
Without DI, a class creates its own dependencies. This creates tight coupling and makes it difficult to replace a real database, payment provider, or email service during testing.
// Tight coupling: the service chooses its implementation
class OrderService {
private final EmailSender sender = new SmtpEmailSender();
}
// Loose coupling: Spring supplies the implementation
@Service
class OrderService {
private final EmailSender sender;
OrderService(EmailSender sender) {
this.sender = sender;
}
}
The second design depends on the EmailSender abstraction.
The implementation can be changed through configuration or a test double
without changing the business class.
Types of dependency injection
| Type | Example | When to use |
|---|---|---|
| Constructor injection | Service(Repository repository) |
Required dependencies; recommended default |
| Setter injection | setFormatter(Formatter formatter) |
Optional or replaceable dependency |
| Field injection | @Autowired private Repository repository |
Generally avoid; hides dependencies and complicates tests |
@Service
class ReportService {
private Formatter formatter;
@Autowired
void setFormatter(Formatter formatter) {
this.formatter = formatter;
}
}
When a class has one constructor, Spring automatically uses it. You
normally do not need @Autowired on that constructor. If a
class has multiple constructors, mark the constructor Spring should use.
Dependency injection and testing
Constructor injection makes a class easy to test without starting Spring. A fake or mock dependency can be passed directly to the constructor.
InvoiceRepository fakeRepository = new InMemoryInvoiceRepository();
InvoiceService service = new InvoiceService(fakeRepository);
// The unit test can exercise InvoiceService without a database
service.createInvoice();
Application Context
The ApplicationContext is Spring's central container. It creates beans, stores them, injects dependencies, publishes events, and manages configuration. In a web application it also connects the application to the embedded server and request infrastructure.
Component Scanning
Component scanning searches the application packages for classes marked
with annotations such as @Component, @Service,
@Repository, and @Controller. Spring registers
each discovered class as a bean.
@SpringBootApplication
public class DemoApplication { }
@Service
class OrderService { }
@Repository
class OrderRepository { }
Keep the main application class in a parent package of these classes. If a component is outside the scan range, Spring will not discover it unless you configure scanning explicitly.
Autowiring
Autowiring is the process of resolving a dependency and supplying it to
a bean. Constructor injection is the clearest form. Use
@Qualifier when more than one bean has the same interface.
interface PaymentGateway {
void pay();
}
@Component("cardGateway")
class CardGateway implements PaymentGateway { public void pay() { } }
@Service
class CheckoutService {
private final PaymentGateway gateway;
CheckoutService(@Qualifier("cardGateway") PaymentGateway gateway) {
this.gateway = gateway;
}
}
What happens when multiple Beans match?
Spring normally resolves a dependency by type. If only one
PaymentGateway Bean exists, it is injected automatically.
If two or more implementations exist, Spring cannot guess which one you
want and may throw NoUniqueBeanDefinitionException.
@Component("cardGateway")
class CardGateway implements PaymentGateway { }
@Component("paypalGateway")
class PaypalGateway implements PaymentGateway { }
// Error: two PaymentGateway Beans match this dependency
CheckoutService(PaymentGateway gateway) { }
Choosing a Bean with @Qualifier
Use @Qualifier when the injection point must select a
specific implementation. The qualifier value normally matches the Bean
name.
@Service
class CheckoutService {
private final PaymentGateway gateway;
CheckoutService(
@Qualifier("paypalGateway")
PaymentGateway gateway) {
this.gateway = gateway;
}
}
Choosing a default Bean with @Primary
Use @Primary when one implementation should be the default
whenever no qualifier is supplied.
@Primary
@Component
class CardGateway implements PaymentGateway { }
@Component
class PaypalGateway implements PaymentGateway { }
// CardGateway is selected because it is @Primary
CheckoutService(PaymentGateway gateway) { }
| Choice | Meaning | Best use |
|---|---|---|
@Qualifier |
Choose one exact Bean at this injection point | The required implementation is known here |
@Primary |
Choose one default Bean among several | One implementation is the normal application choice |
| Both | @Qualifier wins over @Primary |
Use a default but allow explicit overrides |
Injecting all implementations
Spring can inject a List, Set, or
Map of matching Beans. This is useful when an application
should use every notification channel or payment check.
@Service
class NotificationService {
private final List<NotificationSender> senders;
NotificationService(List<NotificationSender> senders) {
this.senders = senders;
}
void notifyAllChannels(String message) {
senders.forEach(sender -> sender.send(message));
}
}
Use @Order on implementations when the execution order
matters.
Optional dependencies
If a dependency is genuinely optional, use Optional or
ObjectProvider instead of allowing a required constructor
dependency to fail at startup.
@Service
class ReportService {
private final Optional<AuditService> auditService;
ReportService(Optional<AuditService> auditService) {
this.auditService = auditService;
}
}
Common DI and autowiring errors
- NoSuchBeanDefinitionException: the required Bean is missing, outside component scanning, or missing an annotation.
-
NoUniqueBeanDefinitionException: multiple Beans
match; use
@Qualifieror@Primary. - Circular dependency: two Beans require each other; redesign the responsibilities or introduce a third service.
-
Null dependency: an object was created with
newinstead of being obtained from Spring, so Spring never injected its dependencies.
@Qualifier for an explicit
choice, use @Primary for a sensible default, and keep
services stateless.
Bean Scopes
A bean scope controls how many instances Spring creates and how long they are kept.
| Scope | Meaning | Good use |
|---|---|---|
singleton |
One instance per application context. This is the default. | Stateless services and repositories |
prototype |
A new instance each time it is requested. | Short-lived, stateful objects |
request |
One instance for each HTTP request. | Request-specific web state |
session |
One instance for each HTTP session. | User-session state |
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
class TemporaryReport { }
Bean Life Cycle
The bean lifecycle is the sequence Spring follows from creating an object to removing it. Knowing the sequence helps you place setup and cleanup code correctly.
- Instantiation: Spring creates the object using its constructor.
- Dependency injection: Spring supplies constructor, field, or setter dependencies.
- Post-processors: framework hooks can inspect or enhance the bean.
-
Initialization: methods marked with
@PostConstructrun after dependencies are ready. - Ready for use: the bean handles application work within its scope.
-
Destruction: during context shutdown,
@PreDestroycan release resources.
@Component
class ConnectionManager {
@PostConstruct
void connect() {
System.out.println("Open connection");
}
@PreDestroy
void disconnect() {
System.out.println("Close connection");
}
}
Use lifecycle callbacks for resources that belong to the bean, such as opening and closing a client. Avoid putting long-running work in initialization because it can delay application startup.
Detailed lifecycle order
For a singleton Bean, Spring applies callbacks in a predictable order. The exact internal path can vary when proxies or custom post-processors are involved, but the following sequence is the important learning model:
- Read the definition: Spring reads metadata from annotations, configuration classes, XML, or a starter.
- Instantiate: Spring calls the constructor and creates the object.
- Aware callbacks: selected callback interfaces receive Spring objects such as the Bean name or context.
- Populate properties: constructor, setter, field, and other dependencies are supplied.
-
Before initialization: registered
BeanPostProcessorimplementations can inspect the Bean. -
Initialize:
@PostConstruct,InitializingBean, and a configuredinitMethodrun. - After initialization: post-processors can wrap the Bean with a proxy, for example for transactions or AOP.
- Use: the ready Bean is used by controllers, services, repositories, or other Beans.
- Shutdown: destruction callbacks release resources when the ApplicationContext closes.
Lifecycle callback options
| Callback | When it runs | Recommendation |
|---|---|---|
@PostConstruct |
After dependency injection | Preferred for small initialization tasks |
InitializingBean.afterPropertiesSet() |
After properties are set | Useful when implementing Spring-specific lifecycle behavior |
@PreDestroy |
Before the Bean is destroyed | Preferred for cleanup tasks |
DisposableBean.destroy() |
During context shutdown | Use when Spring interface coupling is acceptable |
initMethod/destroyMethod |
Configured on a @Bean method |
Useful for third-party classes that cannot be edited |
Initialization interfaces
InitializingBean lets a Bean run code after Spring has
supplied its properties.
@Component
class SearchIndex implements InitializingBean {
private final IndexClient client;
SearchIndex(IndexClient client) {
this.client = client;
}
@Override
public void afterPropertiesSet() {
client.verifyConnection();
}
}
For application code, @PostConstruct is often less coupled
to Spring. Do not implement both for the same setup work, or it may run
twice.
Destruction interfaces
DisposableBean provides a destroy() callback
during a graceful ApplicationContext shutdown.
@Component
class FileResource implements DisposableBean {
private final FileChannel channel;
FileResource(FileChannel channel) {
this.channel = channel;
}
@Override
public void destroy() throws IOException {
channel.close();
}
}
Lifecycle methods for @Bean objects
Use initMethod and destroyMethod when the
object belongs to a third-party library and cannot be annotated.
@Configuration
class ClientConfiguration {
@Bean(initMethod = "connect", destroyMethod = "close")
ExternalClient externalClient() {
return new ExternalClient();
}
}
class ExternalClient {
public void connect() { /* open client */ }
public void close() { /* release client */ }
}
BeanNameAware, BeanFactoryAware, and
ApplicationContextAware
Aware interfaces let a Bean receive information about the Spring container. They are useful for framework infrastructure, but ordinary application services should prefer normal dependency injection.
@Component
class ContainerAwareBean implements
BeanNameAware, BeanFactoryAware,
ApplicationContextAware {
@Override
public void setBeanName(String name) {
System.out.println("Bean name: " + name);
}
@Override
public void setBeanFactory(BeanFactory factory) {
System.out.println("BeanFactory received");
}
@Override
public void setApplicationContext(
ApplicationContext context) {
System.out.println("ApplicationContext received");
}
}
These callbacks are invoked before the initialization callbacks. Direct access to the container increases coupling, so inject the specific dependency you need whenever possible.
BeanPostProcessor
A BeanPostProcessor can run custom logic before and after
every Bean initialization. Spring uses this extension point for
annotations, proxies, AOP, and other framework features.
@Component
class TimingPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(
Object bean, String name) {
System.out.println("Before: " + name);
return bean;
}
@Override
public Object postProcessAfterInitialization(
Object bean, String name) {
System.out.println("After: " + name);
return bean;
}
}
Always return the Bean, or a deliberate replacement/proxy, from these
methods. Returning null can prevent the Bean from
continuing through the processor chain.
BeanFactoryPostProcessor
BeanFactoryPostProcessor runs against Bean definitions
before normal Bean objects are created. It changes metadata such as
property values or scope; it does not perform initialization on a
particular Bean.
@Component
class DefinitionLogger implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(
ConfigurableListableBeanFactory factory) {
System.out.println(
"Definitions: " + factory.getBeanDefinitionCount());
}
}
SmartInitializingSingleton
SmartInitializingSingleton runs after all regular singleton
Beans have been instantiated. It is useful when a component must start
work only after the whole singleton graph is ready.
@Component
class MessageConsumer implements SmartInitializingSingleton {
@Override
public void afterSingletonsInstantiated() {
System.out.println("All singleton Beans are ready");
}
}
Lifecycle and SmartLifecycle
These interfaces manage components that need to start and stop, such as message consumers, schedulers, or background listeners.
@Component
class EventListener implements SmartLifecycle {
private boolean running;
@Override
public void start() {
running = true;
System.out.println("Listener started");
}
@Override
public void stop() {
running = false;
System.out.println("Listener stopped");
}
@Override
public boolean isRunning() {
return running;
}
}
SmartLifecycle also supports automatic startup,
asynchronous shutdown, and phase ordering. Use it for
application-managed processes rather than simple one-time setup.
Prototype and destruction behavior
Spring initializes prototype Beans, but it does not normally call their destruction callbacks because the container does not track every prototype instance after handing it to the application. The code that creates or owns a prototype resource must release it.
Important lifecycle practices
-
Keep
@PostConstructquick; slow network calls can delay application startup. - Close connections, executors, files, and clients in a destruction callback.
- Do not rely on cleanup during a forced process kill; use graceful shutdown where possible.
-
Do not use
ApplicationContextAwareas a replacement for dependency injection. -
Use
BeanPostProcessorandBeanFactoryPostProcessormainly for framework or infrastructure code.
Configuration Properties
Configuration properties keep environment-specific values outside Java
code. They can come from application.properties, YAML
files, environment variables, command-line arguments, or a
profile-specific file.
Using @Value for individual values
@Value is convenient when a Bean needs one or two simple
values. The placeholder uses the format ${property.name}.
# application.properties
app.name=Order Service
app.timeout-seconds=10
@Component
class AppInfo {
@Value("${app.name}")
private String name;
@Value("${app.timeout-seconds:5}")
private int timeoutSeconds;
}
The value after the colon is a default. If
app.timeout-seconds is missing, Spring uses 5.
A missing property without a default usually causes a startup error.
@Value can also read environment variables and simple
expressions, but too many fields make a class difficult to read and
test.
Using @ConfigurationProperties for related settings
Use @ConfigurationProperties when settings belong to one
logical group. Spring binds properties with the same prefix to a typed
object.
# application.properties
app.mail.host=smtp.example.com
app.mail.port=587
app.mail.username=notifications
app.mail.enabled=true
@ConfigurationProperties(prefix = "app.mail")
public record MailProperties(
String host,
int port,
String username,
boolean enabled) { }
Register the class with configuration-property scanning:
@SpringBootApplication
@ConfigurationPropertiesScan
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
It can then be injected like any other Spring Bean:
@Service
class MailService {
private final MailProperties properties;
MailService(MailProperties properties) {
this.properties = properties;
}
void send(String message) {
if (properties.enabled()) {
System.out.println("Sending through " + properties.host());
}
}
}
YAML and nested configuration
YAML is useful when configuration contains nested objects or lists.
app:
name: Order Service
mail:
host: smtp.example.com
port: 587
enabled: true
features:
- orders
- reports
Both kebab-case and camel-case property names can bind to Java fields, but using kebab-case in configuration is a clear convention:
app.mail-timeout=10
@ConfigurationProperties(prefix = "app")
class AppProperties {
private Duration mailTimeout;
// getter and setter
}
Validating configuration
Validation catches incorrect configuration during startup instead of allowing a broken value to fail later during a request. Add the validation starter when it is not already available.
@Validated
@ConfigurationProperties(prefix = "app.mail")
public class MailProperties {
@NotBlank
private String host;
@Min(1)
@Max(65535)
private int port;
// getters and setters
}
If the port is outside the valid range or the host is empty, Spring Boot fails fast and reports the configuration problem.
Creating configuration Beans with @Bean
@Bean registers the return value of a method in the
ApplicationContext. Use it for third-party classes, custom defaults, or
objects that require explicit construction.
@Configuration
class AppConfiguration {
@Bean
Clock applicationClock() {
return Clock.systemUTC();
}
}
@Service
class AuditService {
private final Clock clock;
AuditService(Clock clock) {
this.clock = clock;
}
}
Spring calls the configuration method, stores the returned object as a
Bean, and injects the same Bean wherever Clock is required.
You can give it a name with @Bean("utcClock") and select it
with @Qualifier("utcClock").
Configuring a RestTemplate Bean
RestTemplate is a synchronous HTTP client. It is useful
when a Spring Boot service needs to call another REST service and wait
for its response. Configure it as a Bean so the application can reuse
one consistently configured client.
@Configuration
class HttpClientConfiguration {
@Bean
RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofSeconds(3))
.setReadTimeout(Duration.ofSeconds(5))
.build();
}
}
RestTemplateBuilder is supplied by Spring Boot. The
timeouts prevent a slow or unavailable remote service from holding
application threads forever.
Calling a REST API with RestTemplate
@Service
class CustomerClient {
private final RestTemplate restTemplate;
CustomerClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
Customer findCustomer(Long id) {
return restTemplate.getForObject(
"https://api.example.com/customers/{id}",
Customer.class,
id);
}
}
Common methods include:
| Method | Use |
|---|---|
getForObject |
GET and convert the response body to an object |
getForEntity |
GET and receive status, headers, and body |
postForObject |
POST a request and convert the response |
exchange |
Full control over HTTP method, headers, body, and response type |
delete |
Send a DELETE request |
Sending headers and reading a full response
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<CustomerRequest> request =
new HttpEntity<>(customerRequest, headers);
ResponseEntity<Customer> response = restTemplate.exchange(
url,
HttpMethod.POST,
request,
Customer.class);
Customer customer = response.getBody();
Handling errors from remote services
By default, RestTemplate throws exceptions for many 4xx and
5xx responses. Catch and translate those exceptions at the client
boundary rather than exposing implementation details to controllers.
try {
return restTemplate.getForObject(url, Customer.class);
} catch (HttpClientErrorException.NotFound ex) {
throw new CustomerNotFoundException();
} catch (RestClientException ex) {
throw new RemoteServiceException("Customer API unavailable", ex);
}
RestTemplate or WebClient?
RestTemplate is blocking and easy to understand. For new
applications that need reactive, non-blocking processing, use Spring's
WebClient. Choose based on the architecture rather than
mixing blocking calls into a reactive pipeline.
@Value for a few simple
values, @ConfigurationProperties for related settings,
@Bean for reusable configured objects, and a shared,
timeout-configured HTTP client for external API calls. Never hard-code
passwords, API tokens, or production URLs in Java source code.
Profiles
Profiles let one application use different configuration and Beans for different environments. The same code can run locally, in testing, and in production while using the appropriate database, ports, logging levels, and external services.
spring.profiles.active
Profile-specific property files
Keep shared settings in application.properties and
environment-specific values in files such as
application-dev.properties and
application-prod.properties.
# application.properties - shared by every environment
spring.application.name=order-service
spring.jpa.show-sql=false
# application-dev.properties - safe local settings
server.port=8081
spring.datasource.url=jdbc:h2:mem:orders
logging.level.com.example.orders=DEBUG
# application-prod.properties - production settings
server.port=8080
spring.datasource.url=jdbc:postgresql://db:5432/orders
logging.level.root=INFO
# Select a profile when starting the application
java -jar app.jar --spring.profiles.active=dev
In a deployment environment, the active profile can also be selected with an environment variable:
SPRING_PROFILES_ACTIVE=prod
Using @Profile with Beans
A profile can decide which implementation Spring creates. This is useful when development should use a fake service while production should use a real cloud service.
public interface EmailSender {
void send(String to, String message);
}
@Profile("dev")
@Service
class ConsoleEmailSender implements EmailSender {
public void send(String to, String message) {
System.out.println("DEV email to " + to + ": " + message);
}
}
@Profile("prod")
@Service
class SmtpEmailSender implements EmailSender {
public void send(String to, String message) {
// Send through the production mail provider
}
}
With the dev profile active, Spring registers
ConsoleEmailSender. With the prod profile
active, it registers SmtpEmailSender.
Activating profiles
# application.properties
spring.profiles.active=dev
# Command line (usually useful in deployment scripts)
java -jar app.jar --spring.profiles.active=prod
# Maven test run
mvn test -Dspring.profiles.active=test
Command-line arguments and environment variables are commonly preferred for deployment because the same packaged JAR can be promoted through environments without changing the application code.
Real-world use cases
| Use case | Development | Production |
|---|---|---|
| Database | H2 or a local Docker database | Managed PostgreSQL or MySQL |
| External APIs | Mock server or sandbox endpoint | Live payment, email, or shipping provider |
| Logging | Detailed DEBUG logs |
Concise INFO logs |
| Security | Test users and relaxed local settings | Real authentication and strict security rules |
| Data loading | Sample records for learning | No sample data; use controlled migrations |
Testing with a test profile
A test profile can provide an isolated database and predictable settings so tests do not modify development or production data.
# src/test/resources/application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.hibernate.ddl-auto=create-drop
@ActiveProfiles("test")
@SpringBootTest
class OrderServiceTest {
// Tests use the test database and test configuration
}
Profile groups
When several profiles always belong together, define a profile group:
spring.profiles.group.production=prod,monitoring,security
# Activating production also activates the three related profiles
spring.profiles.active=production
application.properties, put only differences in profile
files, and keep passwords and tokens in environment variables or a
secret manager. Profiles organize configuration; they are not a security
system.
Conditional Beans
Spring Boot can create a bean only when a condition is true. This is the same idea used by auto-configuration and is useful for optional integrations.
@Bean
@ConditionalOnProperty(name = "app.payments.enabled", havingValue = "true")
PaymentGateway paymentGateway() {
return new CardGateway();
}
When to Override Defaults
Keep Boot defaults when they match your needs. Define an explicit bean or property when you need a different port, database, serializer, security rule, or implementation. Prefer a small, deliberate override over disabling large parts of auto-configuration.
--debug flag.
Common Spring Boot Annotations Explained
| Annotation | Purpose | Example usage |
|---|---|---|
@SpringBootApplication |
Main entry point. Combines @Configuration, @EnableAutoConfiguration, and @ComponentScan. | Mark your main class |
@Component |
Generic Spring-managed component. | Generic utility class |
@Service |
Business logic layer. Semantically marks service classes. | Implement business rules |
@Repository |
Data access layer. Can translate database exceptions. | Database access code |
@RestController |
HTTP API controller. Combines @Controller and @ResponseBody. | REST endpoints |
@Bean |
Explicitly register a method's return value as a bean. | Third-party object setup |
@Autowired |
Inject dependencies. (Less common with modern constructor injection) | Field/method injection |
@Value |
Inject a property value into a field. | Single configuration values |
@Configuration |
Class contains @Bean definitions. | Configuration classes |
@ConfigurationProperties |
Bind properties to a typed object. | Group related settings |
Spring Boot Startup Flow
These concepts work together during application startup:
┌─ Application starts ─┐
│ main(String[] args) │
└──────────┬────────────┘
│
▼
┌──────────────────────────────────┐
│ SpringApplication.run() │
│ Creates the Application Context │
└──────────┬───────────────────────┘
│
▼
┌──────────────────────────────────┐
│ Auto-Configuration kicks in │
│ • Checks classpath libraries │
│ • Reads application.properties │
│ • Conditions are evaluated │
└──────────┬───────────────────────┘
│
▼
┌──────────────────────────────────┐
│ Component Scanning │
│ • Finds @Service classes │
│ • Finds @Repository classes │
│ • Finds @RestController classes │
└──────────┬───────────────────────┘
│
▼
┌──────────────────────────────────┐
│ Beans are created │
│ • Instantiate marked classes │
│ • Inject dependencies │
│ • Call @PostConstruct methods │
└──────────┬───────────────────────┘
│
▼
┌──────────────────────────────────┐
│ Embedded Server starts │
│ • Tomcat, Netty, or Jetty │
│ • Listen on configured port │
└──────────┬───────────────────────┘
│
▼
┌──────────────────────────────────┐
│ 🎯 Application Ready! │
│ • Accept requests │
│ • Serve responses │
└──────────────────────────────────┘
Quick Visual: What Spring Boot Does
❌ Without Spring Boot
- Install Tomcat separately
- Create web.xml
- Configure DispatcherServlet
- Set up database connection pool
- Configure Spring context XML
- Manage dependency versions
- Deploy WAR file
- ~400+ lines of configuration
✅ With Spring Boot
- Add starter dependency
- Write your service code
- Write your controller
- Run main() method
- Server starts automatically
- Everything works!
- Run as JAR: java -jar app.jar
- ~50 lines of code + minimal config
Understanding the Request Flow
When a user's browser makes a request to your Spring Boot application, here's what happens:
Client makes HTTP request:
GET /api/users/1
▼
Spring DispatcherServlet receives the request
(automatically configured by Spring Boot)
▼
Request is matched to a @RestController method
Example: getUser(@PathVariable Long id)
▼
Dependencies are injected into controller
(UserService, UserRepository, etc.)
▼
Business logic executes in service layer
(Fetch data from database, apply rules)
▼
Response is converted to JSON
(Spring automatically serializes to JSON)
▼
HTTP response sent back to client
200 OK + JSON body
Spring Boot Core Concepts Visual Guide
These concepts work together during application startup:
SpringApplication.run()
Core Concepts Summary
| Concept | Simple meaning |
|---|---|
| Application Context | The central container that manages Spring Beans. |
| Auto-Configuration | Creates common configuration from dependencies and properties. |
| Starters | Convenient dependency groups for application capabilities. |
| Component Scanning | Finds annotated classes and registers them as Beans. |
| Dependency Injection | Provides required objects instead of creating them manually. |
| Autowiring | Resolves and supplies matching Beans. |
| Bean Scopes | Control how many instances exist and how long they live. |
| Bean Lifecycle | Controls creation, initialization, use, and destruction. |
| Configuration Properties | Keep settings outside Java code. |
| Profiles | Use different settings and Beans for different environments. |
| Conditional Beans | Create Beans only when a condition matches. |
| First Project | Combines these ideas in a working REST API. |
Continue learning
