The framework creates and manages object lifecycles. You declare what you need; the container provides it. DI is the mechanism — dependencies are injected rather than created by the class itself.
// 1. Constructor injection — PREFERRED
@Service
public class PaymentService {
private final AccountRepository repo; // final — immutable, testable
private final FraudChecker fraud;
public PaymentService(AccountRepository repo, FraudChecker fraud) {
this.repo = repo;
this.fraud = fraud;
}
}
// 2. Setter injection — for optional dependencies only
@Autowired(required = false)
public void setMetricsCollector(MetricsCollector collector) { ... }
// 3. Field injection — avoid. Hides dependencies, can't use final, not testable without Spring
@Autowired private AccountRepository repo; // BAD
final → immutable bean, thread-safe.new PaymentService(mockRepo, mockFraud).@Lazy on one dependency, or redesign using an event/observer pattern.@Transactional method → proxy intercepts → opens transaction → calls real method → commits or rolls back.this.method() inside the same class bypasses the proxy entirely — no transaction.@Service
public class OrderService {
public void processBatch(List<Order> orders) {
orders.forEach(this::processOne); // BUG: 'this' bypasses proxy!
}
@Transactional
public void processOne(Order o) { ... }
}
// Fix: extract to a separate bean, OR inject self:
@Autowired private OrderService self;
orders.forEach(self::processOne); // goes through proxy
@Transactional // default: rollback on RuntimeException only
public void transfer() throws BusinessException {
debit();
credit();
throw new BusinessException("limit exceeded"); // does NOT roll back!
}
// Fix:
@Transactional(rollbackFor = BusinessException.class)
@Transactional
public void outer() {
repo.lockRow(id); // holds DB lock
inner(); // opens NEW connection — same row, deadlock!
}
@Transactional(propagation = REQUIRES_NEW)
public void inner() {
repo.readRow(id); // waits for lock held by outer → deadlock
}
REQUIRES_NEW suspends the outer transaction and borrows a new DB connection. Two connections from the same thread fighting for the same row = deadlock.
@Transactional(readOnly = true)
public List<Account> getAccounts() { ... }
// Hibernate skips dirty checking, can route to read replica, skips flush.
// Real performance impact at scale — not just documentation.
Spring AOP uses proxies (not bytecode weaving like AspectJ). This means it only intercepts Spring-managed bean method calls — not constructor calls, field access, or private methods.
@Aspect
@Component
public class PerformanceAspect {
@Around("@annotation(Monitored)")
public Object measure(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long elapsed = System.currentTimeMillis() - start;
log.info("{} took {}ms", pjp.getSignature().getName(), elapsed);
}
}
}
@PreAuthorize in Spring Security is AOP under the hood.@Retryable from Spring Retry is AOP.@Cacheable is AOP — intercepts, checks cache, calls method only on miss.@SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan.@EnableAutoConfiguration imports AutoConfigurationImportSelector.META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (Boot 3+) — a list of hundreds of auto-config classes.@Conditional annotations — only matching ones are loaded.@AutoConfiguration
@ConditionalOnClass(DataSource.class) // JDBC on classpath?
@ConditionalOnProperty("spring.datasource.url") // URL configured?
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean // user didn't define their own DataSource?
public DataSource dataSource(DataSourceProperties props) {
return new HikariDataSource(...);
}
}
@ConditionalOnMissingBean: your bean overrides the default. Auto-configs run after user @Configuration classes, so your beans are already registered when evaluated.@ConditionalOnClass: add a dependency to your POM → behaviour changes automatically.# Run with --debug flag to see what matched and what didn't:
java -jar app.jar --debug
# Or check the conditions report via Actuator:
GET /actuator/conditions
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
// Or in properties:
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
spring.factories and AutoConfiguration.imports?" → Boot 3 moved to the new imports file for performance — scanning the imports file is faster than loading spring.factories. Old spring.factories still works but is deprecated for auto-configs.Constructor
→ @Autowired dependencies injected
→ @PostConstruct
→ InitializingBean.afterPropertiesSet()
→ @Bean(initMethod)
→ ApplicationReadyEvent / ApplicationRunner
─── SERVING TRAFFIC ───
→ @PreDestroy
→ DisposableBean.destroy()
→ @Bean(destroyMethod)
@PostConstruct
public void init() {
// Validate required config (fail fast before serving traffic)
if (props.getApiKey().isBlank()) throw new IllegalStateException("API key required");
// Warm up caches on startup
cache.load(configRepo.findAll());
}
@PreDestroy
public void shutdown() {
// Flush in-flight Kafka messages before JVM exits
producer.flush();
producer.close();
// Drain in-progress jobs
executor.shutdown();
executor.awaitTermination(30, TimeUnit.SECONDS);
}
| Scope | Instances | Use for |
|---|---|---|
singleton | 1 per context | Stateless services (default) |
prototype | New per injection | Stateful objects (report generators) |
request | 1 per HTTP request | Web-scoped state |
session | 1 per HTTP session | User session data |
ObjectFactory<T> or @Lookup to get a fresh instance each time.Spring Security is a chain of Filters, registered as a DelegatingFilterProxy. Every HTTP request passes through the chain in order.
| Filter | Purpose |
|---|---|
| SecurityContextPersistenceFilter | Loads/saves SecurityContext |
| CorsFilter | CORS preflight handling |
| CsrfFilter | CSRF token validation |
| UsernamePasswordAuthenticationFilter | Form login |
| BearerTokenAuthenticationFilter | JWT / OAuth2 bearer token |
| AuthorizationFilter | Role/permission check |
| ExceptionTranslationFilter | Converts security exceptions to HTTP responses |
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // stateless API — no CSRF needed
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
.build();
}
Authorization: Bearer <token>.BearerTokenAuthenticationFilter extracts the token.JwtDecoder validates signature + expiry.Authentication object → stored in SecurityContextHolder.@PreAuthorize("hasRole('ADMIN')") or SecurityContextHolder.getContext().getAuthentication().@Value("${payment.gateway.url}") private String url;
@Value("${payment.gateway.timeout:5000}") private int timeout; // with default
@ConfigurationProperties(prefix = "payment.gateway")
@Validated
public class GatewayProperties {
@NotBlank private String url;
@Min(100) private int timeout = 5000;
@Min(1) private int retries = 3;
private Map<String, String> headers = new HashMap<>();
}
| Feature | @Value | @ConfigurationProperties |
|---|---|---|
| Type safety | No | Yes — validated at startup |
| Grouping | Scattered | One class per config group |
| Relaxed binding | No | Yes — PAYMENT_GATEWAY_URL = payment.gateway.url |
| Maps/Lists | Difficult | Native support |
| IDE autocomplete | No | Yes (with metadata processor) |
@Value for one-off injections. Use @ConfigurationProperties for anything with 3+ related properties or where validation matters.1. Command line args --server.port=9090
2. OS env vars SERVER_PORT=9090
3. application-{profile}.yml
4. application.yml
→ Secrets go in env vars / Vault, never in yml files committed to git.
# application.yml
server:
shutdown: graceful # stop accepting new requests
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # max wait for in-flight
On SIGTERM: Spring stops accepting new requests, waits for in-flight requests to complete (up to timeout), then shuts down. For Kafka consumers, implement SmartLifecycle to stop consuming before the timeout.
# application.yml
management:
endpoint:
health:
probes:
enabled: true # enables /actuator/health/liveness and /readiness
health:
livenessState:
enabled: true
readinessState:
enabled: true
/actuator/health/liveness: "Is the process alive?" Fail → Kubernetes restarts the pod./actuator/health/readiness: "Can it serve traffic?" Fail → removed from load balancer (used during startup and graceful shutdown).@Component
public class KafkaHealthIndicator implements HealthIndicator {
@Override
public Health health() {
try {
kafkaAdmin.describeCluster();
return Health.up().withDetail("broker", "reachable").build();
} catch (Exception e) {
return Health.down().withDetail("error", e.getMessage()).build();
}
}
}
meterRegistry.counter("payments.processed"). Scraped by Prometheus, visualised in Grafana.