Skip to main content

Command Palette

Search for a command to run...

Mastering Spring Framework:1.0

Updated
•10 min read•View as Markdown

    1. Introduction

      1. Problems with Traditional Java

      2. Tight Coupling Explained

      3. Why Spring Was Needed

      4. What is a Bean?

      5. IoC Container (ApplicationContext)

      6. Dependency Injection (DI)

      7. Types of Dependency Injection

      8. Practical Example (Payment System)

      9. Multiple Beans Problem & Solutions

      10. Why Spring Is Better Than new

      11. Conclusion

What is Spring

  • Spring is one of the most widely used Java frameworks for building high-quality, scalable enterprise applications.
    Before understanding why Spring became so popular, let’s first look at the problems it was created to solve.
class OrderService {
    private PaymentService paymentService = new UpiPaymentService();
}

What's wrong with this code?

You might already spot the problem here.

Problems with above code:

1. Tight Coupling

If you want to switch to WalletPaymentService or any other payment service you must edit and recompile OrderService.

This means:

  • No flexibility

  • Hard to replace implementations

  • Hard to extend

2.Hard to test

Testing requires replacing the real implementation with:

  • mock

  • dummy

  • fake

But since the object is created inside the class using new, it's nearly impossible to inject a mock.

3.Scattered Object Creation

Every class creates its own objects.
Object lifecycle & configuration logic ends up being scattered:

  • hard to maintain

  • hard to scale

  • hard to understand

    And Tight Coupling Was Just the Beginning…

  • Inefficient JDBC & ORM integration

  • Scattered configurations

  • Deployment complexity

  • Verbose & repetitive code

  • Manual dependency wiring everywhere

  • Hard-to-test code

And that’s where Spring entered the scene

Spring Framework to the Rescue

Spring introduced:

  • Beans

  • Inversion of Control (IoC)

  • Dependency Injection (DI)

These concepts removed:

  • boilerplate

  • tight coupling

  • manual dependency management

Spring made Java development clean, modular, and testable.

what are Beans :

A Spring Bean is simply a Java object that is: Instantiated by Spring Configured by Spring Managed throughout its lifecycle by Spring Beans

Beans form the backbone of a Spring application. They are the core building blocks that Spring wires together to create the application.

In traditional java we do object creation like PaymentService Psobj = new UPIPaymentService();

like this but in spring we have to create it like this neither we need to use new keyword instead of this we use spring annotation(@component : we will see this in detail ) to tell that to compiler that this is the class and the management of the object of this class is managed by spring

so here’s the first doubt come :

whats the difference between creating object manually like Ps ps = new Ps() and via beans and if beans manages it how it manages and why the need of it

Imagine toys and a toy-box

  • You (the program) want a toy car (an object Ps).

  • One way: you walk to the shelf and say “I want a toy car” and make it yourself: Ps ps = new Ps(); — you built it, you own it, you decide everything about it.

  • Another way: you ask Mom (Spring) to give you a toy car from the toy-box. Mom keeps all toys organized, gives you the right toy when you ask, and can swap a faster toy car later without you changing anything.

In Spring-land, those toys Mom gives are called beans.
Injecting a bean = Mom handing you the toy instead of you creating it yourself.

Why bother? — Short, kid-friendly reasons

  1. Less hard work for you: you don’t need to build toys everywhere (new Ps() everywhere).

  2. Swap toys easily: Mom can replace the toy car with a toy truck and you don’t change your code.

  3. Share toys: Mom can give the same toy to everyone (singleton) or make a new one each time (prototype).

  4. Easier to test: when you want to test, Mom can give a pretend toy (a mock) instead of the real one.

  5. Mom handles rules: Mom can check the toy before giving (initialization), or take it back when done (destruction).

    In technical terms, a Spring Bean is a Java object whose lifecycle is fully managed by the Spring IoC container. Spring is responsible for creating, configuring, injecting, and destroying these objects.

How does Spring know which class to make a Bean?

1.Using @Component (and Stereotype annotations)

1.@Bean

2. @component // Generic bean

3.@Service //buisness logic

4.@Repository //database access

5.@controller // web layer

6.@RestController //web endpoints

@Component
public class PaymentService {
}

2.Using @Bean in a @Configuration Class

@Configuration
public class AppConfig {

    @Bean
    public PaymentService paymentService() {
        return new PaymentService();
    }
}

ex-

@Component
public class OrderService {//<-- BEAN
    private final PaymentService paymentService;

    public OrderService(PaymentService paymentService) {  // injected automatically
        this.paymentService = paymentService;
    }
}

You never write:

new PaymentService();

Spring does everything.

Bean Lifecycle :

Lifecyble methods:

The @PostConstruct annotation is used to mark a method that should be invoked immediately after a bean has been constructed and all of its dependencies have been injected.

The @PreDestroy annotation is used to mark a method that should be invoked just before a bean is destroyed by the container. This method can perform any necessary cleanup or resource releasing tasks.

IOC Container aka Application Context

At the center of everything is the ApplicationContext — Spring’s IoC (Inversion of Control) container.

Think of it as a big organized shelf that stores objects.

When your app starts:

  • Spring scans packages

  • Finds classes with @Component, @Service, etc.

  • Creates objects (Beans)

  • Stores them in the Spring Container

The container is like a box full of ready-to-use objects.

Dependency Injection :How do you use a Bean?

Dependancy Injection is a design pattern in which an object receives its dependencies from an external source (Spring), instead of creating them itself.

This leads to loose coupling, better testability, and cleaner code.

@Autowired
private PaymentService paymentService;

Spring looks in its container:

  • Finds the PaymentService Bean

  • Injects it here

You did NOT use new.


Quick Analogy

Imagine you go to a restaurant:

  • You don’t cook (don’t use new)

  • The chef (Spring) prepares everything

  • You just order what you need (use @Autowired)

The chef-managed food = Beans
The kitchen = Spring Container

1. BEAN = The Object

A bean in Spring is simply:

An object created and managed by Spring.

Examples:

  • UPIPayment object

  • WalletPayment object

  • PaymentService object

  • DatabaseService object

If a class is marked with @Component, @Service, @Repository, or created using @Bean,
Spring adds it to its IoC container (big object box).

Think of it as:

Bean = Toy kept in the toy-box.


2. DI (Dependency Injection) = Giving That Object to Another Class

DI means:

👉 Spring gives the required beans TO YOUR CLASS automatically.

You don’t create the object; Spring gives it to you.

Example:

public PaymentService(PaymentInterface payment) {
    this.payment = payment;   // this is DI
}

Spring looks in its container and says:

“Oh, this class needs a PaymentInterface bean.
I already have UPIPayment or WalletPayment.
Let me inject it.”

Think of it as:

DI = Mom giving you the toy from the toy-box.

Injecting beans means giving one class the object of another class that Spring has already created.

Spring does this using Dependency Injection (DI).

There are 3 ways to inject beans:
1. Constructor Injection (recommended)
2.Field Injection
3.Setter Injection

Let’s understand each one clearly


1. Constructor Injection (BEST way)

Why best?

Immutable
Easy for testing
Avoids null issues
Works smoothly with @Autowired (optional)

Example:

@Service
public class OrderService {

    private final PaymentService paymentService;

    @Autowired   // not required in latest Spring versions
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

Here:

  • Spring creates PaymentService bean

  • Spring creates OrderService bean

  • Spring injects PaymentService into OrderService via constructor


2. Field Injection (not recommended but simplest)

@Service
public class OrderService {

    @Autowired
    private PaymentService paymentService;
}

This works, but:
Harder to test
Not good for clean design
Hides dependencies


3. Setter Injection

Used when the dependency is optional or changeable.

@Service
public class OrderService {

    private PaymentService paymentService;

    @Autowired
    public void setPaymentService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

As we previously seen on creating objects manually we faced problems like tight coupling , hard to test code , scatterd object creation

why to use DI

As you have OrderService class in that you wrote PaymentService Psobj = new UPIPaymentService(); now it you want other payments like wallet payment thats where loose coupling comes into the picture , with loose coupling u get independant testing , application is also much more scalable,you can integrate more and more features without changing or modyfying the previous code that much we will see below how to acheive that using beans and dependancy injection

enough theory lets learn pratically now using the same example

1. Main Application

package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication implements CommandLineRunner {

    private final PaymentService paymentService;

    public DemoApplication(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    @Override
    public void run(String... args) {
        paymentService.pay();
    }
}

2. PaymentInterface

package com.example.demo;
public interface PaymentInterface {
    void pay();
}

3. UPIPayment Implementation

package com.example.demo;
import org.springframework.stereotype.Component;

@Component    // <-- Spring creates an object of this class automatically
public class UPIPayment implements PaymentInterface {

    @Override
    public void pay() {
        System.out.println("Paying through UPI...");
    }
}

4. PaymentService (Dependency is Injected)

package com.example.demo;

import org.springframework.stereotype.Component;

@Component    // Spring creates PaymentService object too
public class PaymentService {

    private final PaymentInterface payment;

    // Constructor Injection
    public PaymentService(PaymentInterface payment) {
        this.payment = payment;
    }

    public void doPayment() {
        payment.pay();
    }
}

NOW — How does Spring actually do all this?

1.Spring scans your project

Because of:

@SpringBootApplication

Spring searches the folder for:

  • @Component

  • @Service

  • @Repository

  • @Controller

and identifies classes it should manage as beans.

Spring says:

“Oh! I found UPIPayment and PaymentService. I will create objects for both.”

Spring creates an IoC Container

IoC Container (Bean Box)
 ├── UPIPayment object
 └── PaymentService object

Spring looks for dependencies

When creating PaymentService, Spring sees:

public PaymentService(PaymentInterface payment)

Spring asks:

“Do I have a bean that implements PaymentInterface?”

Yes → UPIPayment

So Spring:

payment = new UPIPayment();
service = new PaymentService(payment);

and stores them.

When app runs, Spring hands objects to you

You ask for PaymentService

Spring gives you the ready object:

  • With all dependencies filled

  • Already created

  • Ready to use

You never create anything manually

WHY THIS IS BETTER THAN new PaymentService() ?

1. Loose Coupling

If tomorrow you change:

@Component
public class CardPayment implements PaymentInterface {}

Spring will switch implementations with zero code change.

2. Automatic Wiring

You don’t write new anywhere.

3. Test friendly

You can easily write:

new PaymentService(new FakePayment());

4. Lifecycle management

Spring can:

  • initialize beans

  • destroy beans

  • keep one instance

  • create new on each request

5. Perfect for big apps

In real apps you have:

  • DB services

  • Logging

  • Payment Gateways

  • Email Senders

  • Security
    All these need dependency injection.

Now if there are multiple beans and how spring decides which to inject then like we have UPIPaymentService and CardPaymentService how spring chooses which to inject for that

If you define two beans:

@Component
class UpiPaymentService implements PaymentInterface {}

@Component
class CardPaymentService implements PaymentInterface {}

Spring finds two beans implementing the same interface and cannot decide which one to inject, resulting in a NoUniqueBeanDefinitionException.

You must tell Spring which one you want:

Option 1: @Primary

@Primary
@Component
class WalletPaymentService implements PaymentInterface {}

Now Spring injects WalletPaymentService everywhere by default.


Option 2: @Qualifier

@Component
class PaymentService {

    private final PaymentInterface payment;

    public PaymentService(@Qualifier("walletPaymentService") PaymentInterface payment) {
        this.payment = payment;
    }
}

Now Spring knows exactly which bean you want.

Now still if you want fully spring to decide we can add

@ConditionalOnProperty(name="Payment.type" havingvalue="UPI")

and inside application.properties you need to mention

Payment.type=UPI

so now we dont have to change the code to change paymentservice we just have to change in properties or in Env variables

Conclusion

Spring solves the fundamental problems of traditional Java development by introducing Beans, IoC, and Dependency Injection. By removing tight coupling and manual object creation, Spring enables scalable, testable, and maintainable applications.

This is why Spring remains the backbone of modern Java backend development.