Skip to content
Happy Programming Guide
Start learning
Java

Java and the Spring Framework

What Spring actually does: dependency injection, beans and configuration, plus Spring Boot, and how a request travels through a Spring application.

Keys of a mechanical keyboard seen up close

Spring’s core idea is dependency injection — your classes declare what they need and the framework supplies it, rather than each class constructing its own dependencies. Everything else Spring does is built on that. Spring Boot then removes the configuration that historically made Spring intimidating.

The problem it solves#

JAVA
public class OrderService {
    private final EmailSender email = new SmtpEmailSender("smtp.example.com");
    private final OrderRepository repo = new JdbcOrderRepository(dataSource);

    public void place(Order order) {
        repo.save(order);
        email.send(order.getCustomerEmail(), "Order confirmed");
    }
}

Three problems. The class is tied to those exact implementations. Testing it sends real email. And the SMTP host is buried inside business logic.

JAVA
@Service
public class OrderService {
    private final EmailSender email;
    private final OrderRepository repo;

    public OrderService(EmailSender email, OrderRepository repo) {
        this.email = email;
        this.repo = repo;
    }

    public void place(Order order) {
        repo.save(order);
        email.send(order.getCustomerEmail(), "Order confirmed");
    }
}

Now the class states what it needs and someone else decides what to supply. In production Spring supplies the real implementations; in a test you pass fakes. The class itself did not change.

Beans and the container#

A bean is any object Spring manages. At start-up, Spring scans your packages, creates the beans, works out what each one needs, and wires them together. That collection is the application context.

JAVA
@Component     // a generic managed object
@Service       // business logic
@Repository     // data access - also translates database exceptions
@Controller     // handles web requests
@RestController // a controller returning data rather than views

All four of the specific ones are @Component underneath. Using the precise one documents intent and, in the case of @Repository, adds behaviour.

For a class you do not own, declare it in a configuration class:

JAVA
@Configuration
public class AppConfig {

    @Bean
    public ObjectMapper objectMapper() {
        return JsonMapper.builder()
                .addModule(new JavaTimeModule())
                .build();
    }

    @Bean
    public RestClient restClient(RestClient.Builder builder) {
        return builder.baseUrl("https://api.example.com").build();
    }
}

Note that the second method takes a parameter. Spring supplies it — injection works for bean methods as well as constructors.

Constructor injection#

JAVA
// Preferred
@Service
public class OrderService {
    private final OrderRepository repo;

    public OrderService(OrderRepository repo) {
        this.repo = repo;
    }
}

// Avoid
@Service
public class OrderService {
    @Autowired
    private OrderRepository repo;
}

Three reasons the first is better. The field can be final, so it cannot change. The dependency is visible in the signature, so nobody can construct the object in an invalid state. And you can create it in a test with new OrderService(fake), with no Spring involved at all.

The @Autowired annotation on a constructor is optional when the class has only one, which is why you rarely see it in modern code.

When there is more than one candidate#

JAVA
public interface EmailSender { void send(String to, String body); }

@Service
public class SmtpEmailSender implements EmailSender { }

@Service
public class LoggingEmailSender implements EmailSender { }

Spring cannot choose, and start-up fails with an unsatisfied dependency error. Three ways to resolve it:

JAVA
// 1. Mark one as the default
@Service
@Primary
public class SmtpEmailSender implements EmailSender { }

// 2. Name them and ask by name
@Service("logging")
public class LoggingEmailSender implements EmailSender { }

public OrderService(@Qualifier("logging") EmailSender email) { }

// 3. Choose by environment
@Service
@Profile("dev")
public class LoggingEmailSender implements EmailSender { }

You can also inject all of them at once, which is how plugin-style designs work:

JAVA
public NotificationService(List<EmailSender> senders) { }

Scopes#

Scope One instance per
singleton (default) application
prototype injection point
request HTTP request
session user session

Nearly everything is a singleton, which has an important consequence: beans must be stateless. A mutable field on a service is shared by every concurrent request, which is a race condition waiting to happen.

Configuration#

JAVA
@ConfigurationProperties(prefix = "email")
public record EmailProperties(String host, int port, String from) { }
Output
email.host=smtp.example.com
email.port=587
email.from=noreply@example.com
JAVA
@Service
public class SmtpEmailSender implements EmailSender {
    private final EmailProperties props;

    public SmtpEmailSender(EmailProperties props) {
        this.props = props;
    }
}

Typed configuration beats scattered value lookups: a missing or misspelled property fails at start-up rather than at the moment you try to send an email.

Spring Boot#

JAVA
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

That one annotation combines three: component scanning from this package downwards, auto-configuration, and marking the class as a configuration source.

Auto-configuration is the part that feels like magic. Spring Boot looks at what is on your classpath and configures it. A database driver plus a connection URL gives you a working DataSource. The web starter gives you an embedded server. Nothing is generated — it is conditional configuration that activates when the conditions are met.

Terminal
# See exactly what it decided and why
java -jar app.jar --debug

How a request travels#

Output
HTTP request
  -> DispatcherServlet          Spring's single front door
  -> HandlerMapping             which controller method matches this URL
  -> Interceptors and filters   authentication, logging
  -> Controller method          your code
  -> Service                    business logic
  -> Repository                 database
  -> return value
  -> Message converter          object to JSON
  -> HTTP response

Understanding this sequence is what turns Spring from a black box into something you can debug. When a request behaves unexpectedly, you can name the stage to look at.

Transactions#

JAVA
@Service
public class TransferService {

    @Transactional
    public void transfer(long from, long to, BigDecimal amount) {
        accounts.debit(from, amount);
        accounts.credit(to, amount);
    }
}

Both updates commit together or neither does. Two things catch people out: the annotation only works when the method is called from outside the class, because it relies on a proxy; and by default a rollback happens only for unchecked exceptions.

Questions people ask#

What is the difference between Spring and Spring Boot?

Spring is the framework. Spring Boot is a layer on top that configures it with sensible defaults, bundles dependencies into starters, and embeds a web server. Almost nobody starts a new project with plain Spring.

Why is my bean null?

Almost always because you created the object with new instead of letting Spring inject it. Spring only populates beans it manages.

Is Spring heavy?

Start-up takes a few seconds and memory use is higher than a minimal framework. In exchange you get a very large amount of solved problems. Quarkus and Micronaut trade some of that for faster start-up.

Do I need to understand the whole framework?

No. Dependency injection, controllers, services, repositories and configuration properties cover most day-to-day work. Add the rest when you meet a problem that needs it.

Where to go next#

Java web development with Spring BootRead next

Keep reading

Java

Java GUI Programming

Building desktop interfaces in Java: Swing for simplicity, JavaFX for modern applications, layout managers, events, and the threading rule you must not…

4 min read

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *