Constructor vs setter injection, bean scopes and when to use them, prototype behaviour, singleton thread safety, multiple configuration files, design patterns in Spring, profiles, WebFlux vs MVC, and three real-project scenarios.
Published September 25, 2026
The second half of the Spring core questions is about lifecycle and environments: how long beans live, whether they're thread-safe, and how configuration changes between dev and prod. The three scenario questions at the end are common at companies that like "how would you…" rounds.
Short answer: Constructor injection supplies dependencies when the object is created. They're mandatory, can be final, and the object is never half-built. Setter injection supplies them after construction through setter methods. They're optional, and can be changed later.
@Service
class ReportService {
private final ReportRepository repository; // required → constructor
private Clock clock = Clock.systemUTC(); // optional with a default → setter
ReportService(ReportRepository repository) { this.repository = repository; }
@Autowired(required = false)
void setClock(Clock clock) { this.clock = clock; } // tests can override it
}
Key points to cover:
Learn it in depth → Spring Dependency Injection
Short answer:
ServletContext.@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
class RequestContext { private String correlationId; /* … */ }
Common trap: listing "global session". That scope existed only for Portlet applications, and was removed in Spring 5.
Key points to cover:
proxyMode). Otherwise the singleton would capture a single instance forever.Short answer: Singleton for stateless services, repositories, clients and configuration, which is the vast majority of beans. Prototype for objects that hold per-use mutable state and must not be shared: a stateful builder or parser, or a command object that accumulates data.
Short answer: Singleton. Exactly one instance per Spring ApplicationContext, created eagerly at startup (unless marked @Lazy), and shared by everything that injects it.
Key points to cover:
Short answer: Spring doesn't make them thread-safe. It creates one instance and shares it across all request threads. They're safe if they're stateless (only final references to other stateless beans), which is how Controller, Service and Repository beans should be written. Mutable fields in a singleton are shared by every request, and that causes race conditions.
@Service
class PricingService {
private BigDecimal lastPrice; // ❌ shared across all request threads
BigDecimal price(Order o) {
BigDecimal p = calculate(o); // ✅ keep per-request data in local variables
return p;
}
}
Learn it in depth → Synchronized and Locks
Short answer: Yes, and that's normal. Split configuration by concern (SecurityConfig, PersistenceConfig, KafkaConfig), and combine the pieces through component scanning or @Import(...). With XML, use <import resource="…"/>, or pass several files to the context.
Key points to cover:
@Configuration class under the main application's package is picked up automatically, and @Profile can switch whole configuration classes on or off.Short answer:
| Pattern | Where in Spring |
|---|---|
| Singleton | The default bean scope |
| Factory | BeanFactory, FactoryBean, @Bean methods |
| Proxy | AOP: @Transactional, @Cacheable, @Async are implemented with JDK or CGLIB proxies |
| Template Method | JdbcTemplate, RestTemplate, TransactionTemplate |
| Observer | ApplicationEvent and @EventListener |
| Front Controller | DispatcherServlet |
| Strategy | Pluggable PlatformTransactionManager, HandlerMapping and ViewResolver implementations |
| Dependency Injection | The whole container |
Learn it in depth → Spring AOP
Short answer: Every time a prototype bean is requested from the container (by getBean, or by being injected into another bean), Spring creates a new instance and injects its dependencies. Then Spring hands it over and stops managing it: @PreDestroy methods are not called for prototypes.
Common trap: injecting a prototype into a singleton. The injection happens only once, so the singleton keeps the same "prototype" instance forever. To get a fresh instance each time, use ObjectProvider<T> (provider.getObject()), @Lookup methods, or a scoped proxy.
@Service
class ImportService {
private final ObjectProvider<CsvParser> parsers; // CsvParser is @Scope("prototype")
ImportService(ObjectProvider<CsvParser> parsers) { this.parsers = parsers; }
void importFile(Path p) { parsers.getObject().parse(p); } // a new parser per import
}
Short answer: Profiles let you register beans or load configuration only in certain environments, such as dev, test or prod. Annotate beans or configuration classes with @Profile("dev"), put per-environment properties in application-dev.yml, and activate profiles with spring.profiles.active.
@Configuration
@Profile("dev")
class DevDataConfig {
@Bean CommandLineRunner seed(ProductRepository repo) { return args -> repo.saveAll(sampleProducts()); }
}
java -jar app.jar --spring.profiles.active=prod # or SPRING_PROFILES_ACTIVE=prod
Key points to cover:
@Profile("!prod"), @Profile("cloud & kafka").Short answer: WebFlux (Spring 5+) is Spring's reactive, non-blocking web stack, built on Project Reactor (Mono, Flux), and usually run on Netty. Spring MVC is the servlet-based, blocking, thread-per-request stack. WebFlux handles many concurrent connections with a small number of threads, but only if everything in the call chain is non-blocking.
| Spring MVC | Spring WebFlux | |
|---|---|---|
| Model | Blocking, thread per request | Non-blocking, event loop |
| Types | ResponseEntity<Order> | Mono<Order>, Flux<Order> |
| Server | Tomcat, Jetty (Servlet API) | Netty (or servlet containers in async mode) |
| Data access | JDBC/JPA | R2DBC, reactive Mongo/Redis, WebClient |
| Best for | Most CRUD and business apps | Streaming, huge numbers of mostly idle connections, gateways |
Key points to cover:
spring.threads.virtual.enabled=true) gives similar scalability with simple blocking code. That's often the pragmatic choice today.Short answer: Annotations and Java config for virtually all new projects. They're type-safe, refactor-friendly, sit next to the code, and are what Spring Boot is built around. XML survives mainly in legacy systems, or when wiring must change without recompiling (which externalised properties usually handle better).
Key points to cover:
Short answer:
Key points to cover:
ApplicationEventPublisher) to decouple modules that only need to notify each other.Short answer:
ConcurrentHashMap, AtomicLong).synchronized or a ReentrantLock.ThreadLocal for per-thread data, and always clear it in a finally block, because pooled threads are reused.Key points to cover:
Q: What does @Lazy do?
A: It delays creating a singleton bean until it's first needed, instead of at startup. That speeds up startup, but hides configuration errors until runtime. On an injection point, it injects a lazy-resolving proxy.
Q: What are @PostConstruct and @PreDestroy?
A: Lifecycle callbacks. The first runs after dependencies are injected, for initialisation such as warming a cache. The second runs before a singleton is destroyed at shutdown, for cleanup. They come from jakarta.annotation in Spring 6.
Q: How do you read a property value in a bean?
A: @Value("${app.page-size:20}") for single values, with a default after the colon, or a type-safe @ConfigurationProperties(prefix = "app") class (often a record) for groups of related settings.
Q: Can you change the active profile at runtime? A: Not for an already started context. Profiles are evaluated when the context starts. To change configuration at runtime, use Spring Cloud Config with refresh scope, or feature flags.