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:

  1. Tight coupling: Your app knows it must use RazorpayPaymentService.
    Tomorrow your company wants Stripe? You change code and recompile.

  2. 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:

  1. @Primary on the default one:
@Primary
@Service
class StripePaymentService implements PaymentService { ... }
  1. @Qualifier to request a specific one:
Checkout(@Qualifier("razorpayPaymentService") PaymentService payment) { ... }
  1. 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 var
PAYMENT_PROVIDER=stripe (dots → underscores, uppercased).


What actually happens when you click Run

A readable timeline:

  1. JVM calls main() in your @SpringBootApplication class.

  2. SpringApplication.run(...) starts the bootstrapping.

  3. Prepare Environment

    • Reads application.properties / .yml files, environment variables, and command-line args.

    • Figures out active profiles (e.g., dev, prod).

  4. Create ApplicationContext (the bean box) for your app type

    • For web apps, this is a web server–aware context.
  5. 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.

  6. 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!).

  7. Start the embedded server (for web apps)

    • Tomcat/Jetty/Netty starts and binds to a port (8080 by default).
  8. Run runners

    • Executes any CommandLineRunner / ApplicationRunner beans (great for “do X after startup” tasks).
  9. 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

Property sources (rough order of strength):

  • Command-line args (strong)

  • Environment variables

  • application-<profile>.properties

  • application.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)

  • application.properties:

      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.
  • 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.

0
Subscribe to my newsletter

Read articles from Rishika Aggarwal directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rishika Aggarwal
Rishika Aggarwal