Start your SpringBoot Journey !

Why do we even need Spring Boot?
Imagine plain Java:
class RazorpayPaymentService {
String pay() { return "paid via Razorpay"; }
}
public class App {
public static void main(String[] args) {
RazorpayPaymentService svc = new RazorpayPaymentService(); // hard-coded
System.out.println(svc.pay());
}
}
This works, but it has two big issues:
Tight coupling: Your app knows it must use
RazorpayPaymentService
.
Tomorrow your company wants Stripe? You change code and recompile.Manual wiring: You must create every object with
new ...
, pass them around, and handle lifecycles yourself.
Spring (the framework) solves this by creating and managing your objects for you. Spring Boot adds smart defaults so you don’t have to write a mountain of config to get started.
First contact: creating a project (what Spring Initializr really does)
When you visit Spring Initializr, you choose:
Build tool (Maven/Gradle),
Java version,
Packaging (Jar),
Dependencies (for example: Spring Web).
What do “dependencies” mean here?
They’re starter packs. If you add spring-boot-starter-web, you automatically get:
An embedded server (Tomcat by default),
Spring MVC,
A lot of sensible defaults.
You download a zip, unzip, open in your IDE, and you’ll see a class like:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
This single class is the entry point. That one annotation @SpringBootApplication
kicks off scanning, auto-configuration, and bootstrapping. We’ll open that up soon.
Bean, IoC Container, Application Context — what these words mean
A bean is just a Java object that Spring creates and manages.
The IoC container (Inversion of Control container) is Spring’s “box” that holds beans and hands them to you when you need them.
The Application Context is that box in a specific running app. (People often say IoC container, Spring container, and ApplicationContext interchangeably.)
Inversion of Control means: instead of you controlling object creation (new
), Spring controls it. You only declare what you need; Spring supplies it.
What does “managed” cover?
Creation (usually once, as a singleton)
Dependency injection (plugging needed beans into other beans)
Lifecycle (calling init/destroy hooks)
Scopes (singleton by default; others exist)
From “new” to Dependency Injection (DI)
Dependency Injection = “Don’t build your tools; ask Spring to give them to you.”
We decouple our code from the brand and depend on an interface:
public interface PaymentService {
String pay();
}
Two implementations:
import org.springframework.stereotype.Service;
@Service
class RazorpayPaymentService implements PaymentService {
public String pay() { return "paid via Razorpay"; }
}
@Service
class StripePaymentService implements PaymentService {
public String pay() { return "paid via Stripe"; }
}
A class that needs a payment service:
import org.springframework.stereotype.Component;
@Component
class Checkout {
private final PaymentService payment; // depend on the interface
//Constructor Injection (recommended)
Checkout(PaymentService payment) {
this.payment = payment; // Spring injects a bean here
}
void placeOrder() {
System.out.println(payment.pay());
}
}
Why constructor injection is beginner-friendly and robust:
Your fields can be
final
(immutable & always initialized).Easy to test (you can pass a fake in a unit test).
No hidden magic; if a dependency is missing, the app fails to start loudly.
Yes, there is field injection too:
@Autowired
private PaymentService payment;
…but try to prefer constructor injection as your default style.
How does a class become a bean?
Two ways you'll use the most:
(a) Stereotype annotations → auto-detected
Put one of these on the class:
@Component
(generic),@Service
(service layer),@Repository
(data access),@Controller
/@RestController
(web layer)
Spring’s component scanning finds these and registers them as beans.
(b) Manual factory method
When you need to create a third-party object or custom-build something:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class AppConfig {
@Bean
PaymentClient paymentClient() {
return new PaymentClient("api-key");
}
}
Spring will manage the returned object as a bean named paymentClient
.
Component Scanning — how Spring finds beans
Your main class looks like:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@SpringBootApplication
is a meta-annotation that (among other things) triggers @ComponentScan
. By default, scanning starts in the package of this class and includes all its subpackages.
Practical rule: keep your main class in a root package, and put code in subpackages:
com.example.demo ← main class here
├─ controller
├─ service
├─ repository
└─ config
If you place classes outside this tree, Spring won’t find them unless you customize @ComponentScan
.
Auto-Configuration — Spring Boot’s “smart defaults”
Spring Boot looks at:
your classpath (which libraries did you include?),
your properties / environment,
and then auto-registers beans you likely need.
Examples:
Include
spring-boot-starter-web
→ Boot sees MVC & an embedded server → configures a web app.Include JPA + a datasource → Boot configures
EntityManagerFactory
, transactions, etc.
Behind the scenes, Boot discovers auto-config classes via metadata in META-INF
(e.g., an imports list). Each auto-config has conditions, such as:
@ConditionalOnClass(...)
→ only activate if a class is present.@ConditionalOnMissingBean(...)
→ only if you haven’t defined your own.@ConditionalOnProperty(...)
→ only if a property has a value.
You can see which auto-configurations matched by turning on a debug report (more below).
“I have two beans of the same type” — picking the one you want
If Spring finds two PaymentService
beans, it doesn’t know which to inject and fails startup with:
“Expected single matching bean but found 2.”
Ways to resolve:
@Primary
on the default one:
@Primary
@Service
class StripePaymentService implements PaymentService { ... }
@Qualifier
to request a specific one:
Checkout(@Qualifier("razorpayPaymentService") PaymentService payment) { ... }
- Pick by property (most flexible) with conditions:
# application.properties
payment.provider=stripe
@Service
@ConditionalOnProperty(name="payment.provider", havingValue="stripe")
class StripePaymentService implements PaymentService { ... }
@Service
@ConditionalOnProperty(name="payment.provider", havingValue="razorpay")
class RazorpayPaymentService implements PaymentService { ... }
Now you can switch without code changes—flip a property or environment variable.
Tip (env var mapping): payment.provider=stripe
can be set as an env varPAYMENT_PROVIDER=stripe
(dots → underscores, uppercased).
What actually happens when you click Run
A readable timeline:
JVM calls
main()
in your@SpringBootApplication
class.SpringApplication.run
(...)
starts the bootstrapping.Prepare Environment
Reads
application.properties
/.yml
files, environment variables, and command-line args.Figures out active profiles (e.g.,
dev
,prod
).
Create ApplicationContext (the bean box) for your app type
- For web apps, this is a web server–aware context.
Register bean definitions
Component scanning finds your
@Component/@Service/...
classes and registers them (not created yet).Auto-configuration registers more bean definitions based on what it detects on the classpath and properties.
Refresh the context (the big moment)
Instantiates singleton beans.
Injects dependencies into constructors.
Runs bean lifecycle hooks (e.g.,
@PostConstruct
).If a bean is missing or ambiguous, the app fails now (good!).
Start the embedded server (for web apps)
- Tomcat/Jetty/Netty starts and binds to a port (8080 by default).
Run runners
- Executes any
CommandLineRunner
/ApplicationRunner
beans (great for “do X after startup” tasks).
- Executes any
Ready
- Your app is accepting requests. Logs will show what matched and what got created.
Seeing the wiring: logs & the condition report
Turn on a helpful debug report:
# application.properties
debug=true
On startup you’ll see a Condition Evaluation Report section that lists:
Positive matches (auto-configurations and conditions that did match)
Negative matches (those that didn’t, and why)
This is how you verify, for example, that only Stripe’s bean got created when payment.provider=stripe
.
Profiles & properties — switch behavior per environment
Keep shared config in
application.properties
.Environment-specific overrides in
application-dev.properties
,application-prod.properties
, etc.Select one with:
spring.profiles.active=dev
(file, env var, or command line).
Property sources (rough order of strength):
Command-line args (strong)
Environment variables
application-<profile>.properties
Defaults in code
This layering lets you change behavior without touching code.
Manual vs automatic beans — when to use which
Prefer stereotypes (
@Service
,@Component
, …) for your classes.Use
@Bean
methods inside@Configuration
when:You must build a third-party object (e.g., SDK client with API key),
You need fine control (constructor args, customizers),
You want to replace an auto-configured bean with your own.
Remember: auto-configuration backs off if you define a bean of the same type (thanks to @ConditionalOnMissingBean
inside Boot).
A complete mini-walkthrough (end-to-end)
Goal: Choose payment provider at runtime.
Step A — project skeleton
Create a Boot project (Initializr), add Spring Web.
Keep main class at
com.example.demo.DemoApplication
.
Step B — define the interface
public interface PaymentService {
String pay();
}
Step C — two implementations, but only one active at a time
@Service
@ConditionalOnProperty(name="payment.provider", havingValue="stripe")
class StripePaymentService implements PaymentService {
public String pay() { return "paid via Stripe"; }
}
@Service
@ConditionalOnProperty(name="payment.provider", havingValue="razorpay")
class RazorpayPaymentService implements PaymentService {
public String pay() { return "paid via Razorpay"; }
}
Step D — a consumer that doesn’t care which one
@Component
class Checkout {
private final PaymentService payment; // depends on interface only
Checkout(PaymentService payment) { this.payment = payment; }
void placeOrder() {
System.out.println(payment.pay());
}
}
Step E — run something after startup
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
class DemoRunner implements CommandLineRunner {
private final Checkout checkout;
DemoRunner(Checkout checkout){ this.checkout = checkout; }
public void run(String... args) {
checkout.placeOrder();
}
}
Step F — pick the provider with config (no code edits)
-
payment.provider=stripe debug=true
Or via environment variable:
PAYMENT_PROVIDER=razorpay
Run the app. You’ll see either “paid via Stripe” or “paid via Razorpay”, and the debug report will show which bean got created and which one was skipped.
Key lesson: your code depends on ideas (interfaces), and Spring injects the actual parts configured for this environment.
Common mistakes and how to fix them
“No qualifying bean of type X”
Did you put
@Service/@Component
on the class?Is it inside the scanned package tree?
For manual beans, did you write a
@Bean
method?
“Expected single matching bean but found 2”
- Use
@Primary
,@Qualifier
, or@ConditionalOnProperty
to pick one.
- Use
NPE after removing
new
You stopped manually creating the object (good), but didn’t let Spring inject it.
Switch to constructor injection and ensure the dependency is a bean.
My controller/service not picked up
- Check package structure. The main class’s package is the scan root.
Tiny glossary (revisited)
Bean: an object Spring creates and manages.
DI: Spring supplies your dependencies; you don’t
new
them.IoC/ApplicationContext: the container that holds all beans.
Stereotypes:
@Component/@Service/@Repository/@Controller
—mark classes to auto-register as beans.@Bean
: a factory method that returns an object to register as a bean.Auto-Configuration: Boot’s feature that adds typical beans based on classpath and properties.
Profiles: named configurations like
dev
,test
,prod
you can switch between.
14) Mental model to keep in your head
Spring builds a “bean box.”
Your classes say what they need (interfaces).
Component scanning & auto-config fill the box with beans.
When your app starts, Spring injects the right beans into the right places.
You change behavior by changing properties, not code.
Subscribe to my newsletter
Read articles from Rishika Aggarwal directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
