Skip to main content

Chapter 4: Scopes and Configuration

Introduction​

You know how to create Beans, but do you know how many instances Spring creates? Just one for the entire application? A new one each time?

This chapter teaches you how to control the lifecycle of Beans with Scopes.

What You Will Learn​

  1. Bean scopes (Singleton, Prototype, etc.)
  2. When to use which scope
  3. Configuration with @Configuration
  4. Profiles (@Profile) for dev/test/prod
  5. @ComponentScan

Duration: 45 minutes


Bean Scopes​

What is a Scope?​

Scope = Lifetime and number of instances of a Bean

Question: When Spring creates UserService, how many instances does it create?

  • Only one for the entire application? (Singleton)
  • A new one for each injection? (Prototype)
  • One per HTTP request? (Request)
  • One per user session? (Session)

The scope determines the answer!


The 5 Available Scopes​

ScopeDescriptionInstancesUsage
@Singleton1 instance for the entire application1Default - Services, Repositories
@PrototypeNew instance for each injectionMultipleStateful objects
@RequestScope1 instance per HTTP request1/requestObjects related to a request
@SessionScope1 instance per user session1/sessionCart, user preferences
@ApplicationScope1 instance for the web application1Similar to Singleton for web

1. Singleton Scope (Default)​

Principle​

Spring creates ONLY 1 instance of the Bean
All components share the SAME instance

Code​

@Service  // Singleton by default
public class UserService {

private int counter = 0;

public void incrementCounter() {
counter++;
}

public int getCounter() {
return counter;
}
}

Test​

@RestController
public class TestController {

private final UserService userService;

public TestController(UserService userService) {
this.userService = userService;
}

@GetMapping("/test")
public String test() {
userService.incrementCounter();
return "Counter: " + userService.getCounter();
}
}

Result:

Request 1: Counter: 1
Request 2: Counter: 2
Request 3: Counter: 3
...

Why? It's the SAME instance for all requests!


Visualization​

All share the same instance!


When to Use Singleton?​

✅ Stateless services

@Service
public class EmailService {
public void send(String to, String message) {
// No mutable instance variables
// Just methods working with parameters
}
}

✅ Repositories

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// No state
}

✅ Controllers

@RestController
public class UserController {
// No state (dependencies are final)
}

Rule: If the Bean has no mutable state, use Singleton (default)!


2. Prototype Scope​

Principle​

Spring creates a NEW instance each time the Bean is requested
Each component has its OWN instance

Code​

import org.springframework.context.annotation.Scope;

@Service
@Scope("prototype") // ← New instance each time
public class ShoppingCart {

private List<Product> items = new ArrayList<>();

public void addProduct(Product product) {
items.add(product);
}

public List<Product> getItems() {
return items;
}
}

Test​

@RestController
public class CartController {

private final ApplicationContext context;

public CartController(ApplicationContext context) {
this.context = context;
}

@GetMapping("/test-prototype")
public String testPrototype() {
// Retrieve 2 instances
ShoppingCart cart1 = context.getBean(ShoppingCart.class);
ShoppingCart cart2 = context.getBean(ShoppingCart.class);

// Modify cart1
cart1.addProduct(new Product("Laptop"));

// Check cart2
return "Cart1: " + cart1.getItems().size() +
", Cart2: " + cart2.getItems().size();
}
}

Result:

Cart1: 1, Cart2: 0

Why? They are DIFFERENT instances!


Visualization​

Each component has its own instance!


When to Use Prototype?​

✅ Stateful objects

@Scope("prototype")
public class ReportGenerator {
private String title;
private List<Data> data;
// State specific to each report
}

✅ Temporary objects

@Scope("prototype")
public class FileProcessor {
private File currentFile;
// Processing a specific file
}

⚠️ Warning: Prototype with Singleton = problem!


Problem: Prototype in Singleton​

@Service  // Singleton
public class OrderService {

private final ShoppingCart cart; // Prototype

// ❌ PROBLEM!
public OrderService(ShoppingCart cart) {
this.cart = cart;
// cart is injected ONLY ONCE (at startup)
// Even if ShoppingCart is Prototype!
}
}

Result: The Prototype behaves like a Singleton!

Solution: Use ObjectProvider or ApplicationContext.getBean() to retrieve a new instance each time.


3. Request Scope (Web)​

Principle​

1 new instance PER HTTP request
The instance exists throughout the request
Destroyed after the response

Code​

import org.springframework.web.context.annotation.RequestScope;

@Component
@RequestScope // ← New instance per request
public class RequestLogger {

private String requestId;
private LocalDateTime startTime;

@PostConstruct
public void init() {
this.requestId = UUID.randomUUID().toString();
this.startTime = LocalDateTime.now();
System.out.println("Request " + requestId + " started");
}

@PreDestroy
public void cleanup() {
Duration duration = Duration.between(startTime, LocalDateTime.now());
System.out.println("Request " + requestId + " completed in " +
duration.toMillis() + "ms");
}
}

Usage​

@RestController
public class UserController {

private final RequestLogger logger;

public UserController(RequestLogger logger) {
this.logger = logger;
}

@GetMapping("/users")
public List<User> getUsers() {
logger.log("Fetching users");
// ... logic
}
}

Result:

Request 1:
Request abc-123 started
Request abc-123 completed in 45ms

Request 2:
Request def-456 started
Request def-456 completed in 32ms

Each request has its own instance!


4. Session Scope (Web)​

Principle​

1 instance per user session
Shared among all requests of the same user
Destroyed when the session expires

Code​

import org.springframework.web.context.annotation.SessionScope;

@Component
@SessionScope // ← Instance per user session
public class UserPreferences {

private String theme = "light";
private String language = "en";

public void setTheme(String theme) {
this.theme = theme;
}

public String getTheme() {
return theme;
}
}

Usage: Shopping cart, preferences, authentication


Comparison of Scopes​

Summary Table​

ScopeInstancesLifetimeMain Usage
Singleton1 for the appStartup → ShutdownServices, Repos (default)
Prototype1 per lookupCreation → GCStateful objects
Request1 per HTTP requestRequest → ResponseLogs, request context
Session1 per user sessionSession start → endCart, preferences

Temporal Visualization​

Singleton:     [════════════════════════════════] (entire app)

Prototype: [===] [===] [===] [===] [===] (on demand)

Request: [req1] [req2] [req3] [req4] (per request)

Session: [═══ User1 Session ═══]
[═══════ User2 Session ═══════]

Configuration with @Configuration​

Complete Configuration Class​

@Configuration
public class AppConfig {

// Singleton Bean (default)
@Bean
public EmailService emailService() {
EmailService service = new EmailService();
service.setHost("smtp.gmail.com");
return service;
}

// Prototype Bean
@Bean
@Scope("prototype")
public ReportGenerator reportGenerator() {
return new ReportGenerator();
}

// Conditional Bean (explained below)
@Bean
@Profile("dev")
public DataSource devDataSource() {
return new H2DataSource();
}

@Bean
@Profile("prod")
public DataSource prodDataSource() {
return new PostgreSQLDataSource();
}
}

Profiles (@Profile)​

Problem​

You have different configurations depending on the environment:

Development:
- In-memory H2 database
- Detailed logs
- Fake email mode

Production:
- PostgreSQL database
- Minimal logs
- Real email

How to manage this cleanly?


Solution: Spring Profiles​

Create Beans based on the active profile:

@Configuration
public class DatabaseConfig {

@Bean
@Profile("dev") // ← Active only in dev
public DataSource devDataSource() {
System.out.println("Using H2 (dev)");
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}

@Bean
@Profile("prod") // ← Active only in prod
public DataSource prodDataSource() {
System.out.println("Using PostgreSQL (prod)");
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("admin");
config.setPassword("secret");
return new HikariDataSource(config);
}

@Bean
@Profile("test") // ← Active only in test
public DataSource testDataSource() {
System.out.println("Using H2 (test)");
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("test-schema.sql")
.build();
}
}

Activate a Profile​

Method 1: application.properties

spring.profiles.active=dev

Method 2: Environment Variable

export SPRING_PROFILES_ACTIVE=prod

Method 3: JVM Argument

java -jar app.jar --spring.profiles.active=prod

Method 4: IDE (VS Code, IntelliJ)

"spring.profiles.active": "dev"

Multiple Profiles​

Activate multiple profiles:

spring.profiles.active=dev,debug,mock-email

Bean active for multiple profiles:

@Bean
@Profile({"dev", "test"}) // ← Active in dev OR test
public DataSource devOrTestDataSource() {
return new H2DataSource();
}

Complete Example: Environment Configuration​

// Common interface
public interface EmailService {
void send(String to, String message);
}

// DEV Implementation (fake)
@Service
@Profile("dev")
public class FakeEmailService implements EmailService {
@Override
public void send(String to, String message) {
System.out.println("FAKE EMAIL to " + to + ": " + message);
}
}

// PROD Implementation (real)
@Service
@Profile("prod")
public class RealEmailService implements EmailService {
@Override
public void send(String to, String message) {
// Real sending logic via SMTP
smtpClient.send(to, message);
}
}

In dev: No real emails sent
In prod: Real emails sent


Configuration Files by Profile​

Structure​

src/main/resources/
├── application.properties (common)
├── application-dev.properties (dev only)
├── application-test.properties (test only)
└── application-prod.properties (prod only)

Example​

application.properties (common):

app.name=My Application
server.port=8080

application-dev.properties:

spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.show-sql=true
logging.level.root=DEBUG

application-prod.properties:

spring.datasource.url=jdbc:postgresql://prod-server:5432/mydb
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
spring.jpa.show-sql=false
logging.level.root=WARN

Activation:

spring.profiles.active=dev

Spring automatically loads application-dev.properties!


@ComponentScan​

Problem​

By default, Spring only scans the package of the main class and its sub-packages.

com.example.demo
├── DemoApplication.java ← Main class
├── service/
│ └── UserService.java ✅ Scanned
└── controller/
└── UserController.java ✅ Scanned

com.external.library
└── ExternalService.java ❌ NOT scanned

Solution: @ComponentScan​

@SpringBootApplication
@ComponentScan(basePackages = {
"com.example.demo", // Main package
"com.external.library" // External package
})
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

Now ExternalService will be scanned!


Filter the Scan​

Exclude certain classes:

@ComponentScan(
basePackages = "com.example.demo",
excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {OldService.class, DeprecatedController.class}
)
)

Include only certain annotations:

@ComponentScan(
basePackages = "com.example.demo",
includeFilters = @ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = Service.class // Only @Service
),
useDefaultFilters = false
)

Complete Example: Multi-Environment Application​

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

// Configuration
@Configuration
public class AppConfig {

// Email based on environment
@Bean
@Profile("dev")
public EmailService devEmailService() {
return new FakeEmailService();
}

@Bean
@Profile("prod")
public EmailService prodEmailService() {
return new SmtpEmailService();
}

// Cache based on environment
@Bean
@Profile("prod")
@Scope("singleton")
public CacheManager redisCacheManager() {
return new RedisCacheManager();
}

@Bean
@Profile("dev")
@Scope("singleton")
public CacheManager simpleCacheManager() {
return new SimpleCacheManager();
}
}

// Service with appropriate scope
@Service
@Scope("singleton") // One instance for the app
public class OrderService {

private final EmailService emailService;
private final CacheManager cacheManager;

public OrderService(EmailService emailService,
CacheManager cacheManager) {
this.emailService = emailService;
this.cacheManager = cacheManager;
}
}

// Bean with state (Prototype)
@Component
@Scope("prototype") // New instance each time
public class Invoice {
private Order order;
private BigDecimal total;

public void generate(Order order) {
this.order = order;
this.total = calculateTotal(order);
}
}

Chapter Summary​

Scopes​

@Scope("singleton")   // 1 instance (default)
@Scope("prototype") // N instances
@RequestScope // 1 per HTTP request
@SessionScope // 1 per user session

Profiles​

@Profile("dev")       // Active in dev
@Profile("prod") // Active in prod
@Profile({"dev","test"}) // Active in dev OR test

Activation:

spring.profiles.active=dev

Configuration​

@Configuration        // Config class
@Bean // Create a Bean
@ComponentScan // Scan packages

Key Points​

✅ Singleton by default (stateless services)
✅ Prototype for stateful objects
✅ Request/Session for web
✅ @Profile to manage dev/test/prod
✅ application-{profile}.properties for config
✅ @ComponentScan for external packages


Self-Assessment​

  1. What is the difference between Singleton and Prototype?
  2. When to use RequestScope?
  3. How to activate a Spring profile?
  4. What is the difference between application.properties and application-dev.properties?
  5. What is the purpose of @ComponentScan?

Congratulations! 🎉​

You have completed all the theory of Module 5!

You now understand:

  • ✅ Inversion of Control (IoC)
  • ✅ Dependency Injection (DI)
  • ✅ Spring Beans
  • ✅ Scopes and Configuration

Next step: Practice with the labs!


➡️ Start Lab 1: Advanced Injection

⬅️ Previous Chapter: Beans

⬅️ Back to Module 5