← Series overview

Spring Boot from first principles · Module 04 of 05

Understand how Spring finds and creates beans.

Stereotypes are convenient for application classes, but not every dependency can or should be annotated. Explicit configuration gives the application a clear place to construct third-party and policy-driven objects.

After this module, you should be able to:
  • state exactly which packages the default component scan searches
  • choose between a component stereotype and an explicit @Bean method
  • use method-parameter injection safely with proxyBeanMethods=false
  • inject Clock so time-dependent code is deterministic and testable

Discovery boundary

Component scanning starts at the application package.

With OrdersApplication in com.example.orders, the default scan searches that package and every subpackage. It finds classes such as:

Inside the default scan
com.example.orders.notification.EmailNotificationSender
com.example.orders.service.OrderService
com.example.orders.configuration.TimeConfiguration
com.example.orders.runner.DemoRunner

It does not automatically scan a sibling such as com.example.payments. Put the application class at a sensible package root instead of widening the scan without a reason.

Scanning reads class metadata. It is looking for component stereotypes and configuration classes, not for every Java class in the package. An unannotated DTO in the same folder is visible to Java but does not become a bean merely because the scanner passed over it.

Same package, different result
package com.example.orders;

@Service
class OrderService { }
// registered by component scanning

class CreateOrderRequest { }
// ordinary class; no bean definition is registered

Two creation paths

Use scanning for your components and @Bean for explicit construction.

Stereotype annotation

Use @Component, @Service or @Repository when you own the class and its role belongs in the application.

@Bean factory method

Use a method in @Configuration when the class comes from a library, construction needs explicit values, or the application should own the creation policy.

Explicit bean definitions
@Configuration(proxyBeanMethods = false)
public class TimeConfiguration {

    @Bean
    public Clock applicationClock() {
        return Clock.systemUTC();
    }

    @Bean
    public OrderTimestampService orderTimestampService(
            Clock applicationClock
    ) {
        return new OrderTimestampService(applicationClock);
    }
}

The default bean names here are applicationClock and orderTimestampService, taken from the method names. Spring resolves the Clock parameter before invoking the second method.

@Configuration is itself a component stereotype, so the scan finds TimeConfiguration. Spring then processes its @Bean methods as additional definitions.

  1. 01

    Find TimeConfiguration

    The default component scan discovers the configuration class.

  2. 02

    Register both method definitions

    The declared return types tell Spring that a Clock and an OrderTimestampService can be created.

  3. 03

    Resolve method parameters

    Spring finds the managed Clock before calling the service factory method.

  4. 04

    Register returned objects as beans

    The object returned from each method is managed by the context.

Choosing the creation path

The decision is about ownership and construction, not class importance.

ObjectLikely definition styleReason
OrderService@ServiceYou own the application class and its service role is clear.
Clock@BeanIt is a JDK class and cannot be edited to add a Spring annotation.
Vendor SDK client@BeanConstruction needs credentials, endpoints or a builder you control.
CreateOrderRequestNeitherEach request is ordinary input data, not a shared collaborator.

Avoid defining the same application service both with @Service and an unrelated @Bean method unless two distinct definitions are intentional. Otherwise the context may contain two candidates of the same type and injection becomes ambiguous.

Accidental duplicate definitions
@Service
class OrderService { }

@Bean
OrderService orderService() {
    return new OrderService();
}

// These are two bean definitions, not two annotations
// describing one shared instance.

proxyBeanMethods

With proxies disabled, direct method calls are ordinary Java calls.

@Configuration(proxyBeanMethods = false) avoids method interception. It is efficient and clear when bean methods are independent or receive other beans as parameters.

Prefer parameter injection
// Correct: Spring supplies its managed TokenGenerator bean.
@Bean
public OtpService otpService(TokenGenerator tokenGenerator) {
    return new OtpService(tokenGenerator);
}

// Risky with proxyBeanMethods = false:
@Bean
public OtpService anotherOtpService() {
    return new OtpService(tokenGenerator());
    // tokenGenerator() is a normal Java call and creates another instance.
}

The default @Configuration mode uses a proxy that can intercept calls between @Bean methods and return the managed bean. Parameter injection works in either mode and states the dependency more directly.

proxyBeanMethods = true

Inter-bean calls are intercepted

A direct call such as tokenGenerator()can be redirected to the context's managed singleton.

proxyBeanMethods = false

Methods have normal Java semantics

Calling tokenGenerator() executes its body and constructs whatever that method normally returns.

See the duplicate construction explicitly
@Configuration(proxyBeanMethods = false)
class TokenConfiguration {

    @Bean
    TokenGenerator tokenGenerator() {
        return new TokenGenerator();
    }

    @Bean
    OtpService otpService() {
        return new OtpService(tokenGenerator());
    }
}

// Context bean A: created from tokenGenerator()
// Plain object B: created when otpService() calls tokenGenerator()
// OtpService receives B, not the managed bean A.
One managed instance in either proxy mode
@Bean
OtpService otpService(TokenGenerator tokenGenerator) {
    return new OtpService(tokenGenerator);
}

Managed and ordinary objects

Spring should manage services and infrastructure, not every object.

Spring-managed objects participate in injection, scopes and lifecycle callbacks. Short-lived domain values—request DTOs, entities, value objects and exceptions—are normally created by application or persistence code. They do not need to be fetched from the ApplicationContext.

When a managed service needs another managed object, constructor injection is the normal path. Direct context access remains an integration escape hatch, not the default application design.

Usually managed

Collaborators and infrastructure

Services, repositories, clients, clocks, transaction infrastructure and configuration-driven strategies.

Usually ordinary

Values and per-operation state

DTOs, entities, order lines, identifiers, exceptions and local collections.

Creation location determines ownership
@Bean
Clock applicationClock() {
    return Clock.systemUTC();
}
// The returned Clock is a bean because Spring invokes
// this definition and registers the result.

Instant timestamp = applicationClock.instant();
// The Instant is a normal value returned during business work.
// It does not become a bean.

Calling new inside a @Bean method is completely normal: the returned object is precisely what Spring is being asked to manage. Calling new inside a service for one of its required collaborators bypasses injection and recreates the coupling from module 1.

Inject time

Clock turns the current time into a replaceable dependency.

A deterministic Clock
Clock fixedClock = Clock.fixed(
        Instant.parse("2026-08-31T00:00:00Z"),
        ZoneOffset.UTC
);

Clock.fixed(...) always returns the same instant. That is useful in this learning checkpoint and in tests. A production bean usually returns Clock.systemUTC(), while a test configuration supplies a fixed clock.

The service has no idea which clock it received
public class OrderTimestampService {
    private final Clock clock;

    public OrderTimestampService(Clock clock) {
        this.clock = clock;
    }

    public Instant currentTimestamp() {
        return clock.instant();
    }
}

System UTC

Clock.systemUTC()

Returns the current real time in UTC.

Fixed

Clock.fixed(
    instant,
    ZoneOffset.UTC
)

Always returns one instant, ideal for deterministic assertions.

Offset

Clock.offset(
    baseClock,
    Duration.ofMinutes(5)
)

Returns time shifted from another clock.

A test without waiting for real time
Clock testClock = Clock.fixed(
        Instant.parse("2026-08-31T00:00:00Z"),
        ZoneOffset.UTC
);
OrderTimestampService service =
        new OrderTimestampService(testClock);

assertEquals(
        Instant.parse("2026-08-31T00:00:00Z"),
        service.currentTimestamp()
);

The service is tested as plain Java. Spring configuration decides which clock production receives; the class itself remains independent of the container.

When direct context access is justified

Use ApplicationContext at integration boundaries, not as hidden injection.

SituationNormal choiceWhy
A service always needs one repositoryConstructor injectionThe dependency is stable and known when the service is created.
Bootstrapping framework integrationContext access may be reasonableThe boundary's job may be to connect Spring with external lifecycle code.
Runtime plugin chosen from an open-ended setDynamic lookup may be reasonableThe exact implementation may genuinely be unknown at construction time.
A method hides getBean for convenienceAvoid itIt hides dependencies and couples ordinary business code to Spring.

Even dynamic selection often has a cleaner injectable form, such as injecting a map of strategies into a dedicated registry. Reach for the context only when the boundary truly needs container-level behaviour.

Practice checkpoint

Write it before reading the solution.

Suggested time: 15 minutes

Add deterministic timestamps to the existing non-web application.

  • Create an OrderTimestampService that depends on Clock.
  • Define a fixed UTC clock and the service in explicit configuration.
  • Keep proxyBeanMethods = false.
  • Print the timestamp from the existing runner.
Hints, in increasing order
  1. OrderTimestampService needs imports for both Clock and Instant.
  2. Create the clock and service through two @Bean methods.
  3. Inject Clock as a parameter to the service bean method; do not call applicationClock().
  4. Add OrderTimestampService to all three places in the runner constructor: parameter, assignment and field.
Working solution
OrderTimestampService.java
package com.example.orders.time;

import java.time.Clock;
import java.time.Instant;

public class OrderTimestampService {
    private final Clock applicationClock;

    public OrderTimestampService(Clock applicationClock) {
        this.applicationClock = applicationClock;
    }

    public Instant currentTimestamp() {
        return applicationClock.instant();
    }
}
TimeConfiguration.java
package com.example.orders.configuration;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.example.orders.time.OrderTimestampService;

@Configuration(proxyBeanMethods = false)
public class TimeConfiguration {

    @Bean
    public Clock applicationClock() {
        return Clock.fixed(
                Instant.parse("2026-08-31T00:00:00Z"),
                ZoneOffset.UTC
        );
    }

    @Bean
    public OrderTimestampService orderTimestampService(
            Clock applicationClock
    ) {
        return new OrderTimestampService(applicationClock);
    }
}
DemoRunner.java
package com.example.orders.runner;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import com.example.orders.service.OrderService;
import com.example.orders.service.OtpService;
import com.example.orders.time.OrderTimestampService;

@Component
public class DemoRunner implements CommandLineRunner {
    private final OrderService orderService;
    private final OtpService otpService;
    private final OrderTimestampService orderTimestampService;

    public DemoRunner(
            OrderService orderService,
            OtpService otpService,
            OrderTimestampService orderTimestampService
    ) {
        this.orderService = orderService;
        this.otpService = otpService;
        this.orderTimestampService = orderTimestampService;
    }

    @Override
    public void run(String... args) {
        orderService.placeOrder("ORD-201", "customer@example.com");
        otpService.sendOtp("+919876543210", "482731");
        System.out.println(
                "TIMESTAMP: " + orderTimestampService.currentTimestamp()
        );
    }
}
Additional output
TIMESTAMP: 2026-08-31T00:00:00Z
Record after the attempt

Elapsed time · hint level (H0 means no hint; H1–Hn is the last hint used) · conceptual errors · mechanical errors · explanation confidence from 0–2.

Foundation check

Who owns each object?

Spring owns the configured Clock and OrderTimestampService beans. The Instant returned by the service is an ordinary value object created by the clock; it is not a bean.