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
- Bean scopes (Singleton, Prototype, etc.)
- When to use which scope
- Configuration with @Configuration
- Profiles (@Profile) for dev/test/prod
- @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
| Scope | Description | Instances | Usage |
|---|---|---|---|
@Singleton | 1 instance for the entire application | 1 | Default - Services, Repositories |
@Prototype | New instance for each injection | Multiple | Stateful objects |
@RequestScope | 1 instance per HTTP request | 1/request | Objects related to a request |
@SessionScope | 1 instance per user session | 1/session | Cart, user preferences |
@ApplicationScope | 1 instance for the web application | 1 | Similar 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
| Scope | Instances | Lifetime | Main Usage |
|---|---|---|---|
| Singleton | 1 for the app | Startup → Shutdown | Services, Repos (default) |
| Prototype | 1 per lookup | Creation → GC | Stateful objects |
| Request | 1 per HTTP request | Request → Response | Logs, request context |
| Session | 1 per user session | Session start → end | Cart, 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
- What is the difference between Singleton and Prototype?
- When to use RequestScope?
- How to activate a Spring profile?
- What is the difference between application.properties and application-dev.properties?
- 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!