Overview
Spring Boot's "magic" is well-defined machinery: auto-configuration is conditional bean registration, @Transactional is a CGLIB proxy, and AOP is method interception via dynamic proxies. Understanding the mechanics — not just the annotations — lets you predict behavior in edge cases (self-invocation bypassing transactions, prototype beans in singletons, circular dependencies).
Architecture
SPRING BOOT STARTUP SEQUENCE
════════════════════════════════════════════════════════
1. SpringApplication.run()
2. Create ApplicationContext (AnnotationConfigServletWebServerAppContext)
3. Load @SpringBootApplication → triggers @ComponentScan + @EnableAutoConfiguration
4. Scan classpath for /META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
5. Evaluate @Conditional* on each AutoConfiguration class:
@ConditionalOnClass(DataSource.class) → DataSource on classpath?
@ConditionalOnMissingBean(DataSource.class) → user not already defined one?
6. Register qualifying beans → BeanFactory
7. BeanPostProcessor runs on each bean (AOP, @Transactional, @Cacheable wiring)
8. @PostConstruct methods called
9. ApplicationReadyEvent fired → app is live
BEAN LIFECYCLE
════════════════════════════════════════════════════════
Instantiation (constructor)
│
Populate @Autowired fields
│
BeanNameAware.setBeanName()
ApplicationContextAware.setApplicationContext()
│
BeanPostProcessor.postProcessBeforeInitialization() ← AOP proxy created here
│
@PostConstruct (or InitializingBean.afterPropertiesSet())
│
BeanPostProcessor.postProcessAfterInitialization()
│
──── Bean is READY ────
│
@PreDestroy (or DisposableBean.destroy()) on shutdown
@TRANSACTIONAL PROXY MECHANICS
════════════════════════════════════════════════════════
External caller → CGLIB Proxy → Your method
CGLIB Proxy (generated subclass):
void createOrder(Order o) {
txManager.beginTransaction(); // ← injected behavior
try {
super.createOrder(o); // ← YOUR actual code
txManager.commit();
} catch(RuntimeException e) {
txManager.rollback();
throw e;
}
}
SELF-INVOCATION BYPASS:
class OrderService {
public void placeOrder() {
this.createOrder(); ← calls real object, not proxy → NO transaction!
}
@Transactional
public void createOrder() { ... }
}
Fix: inject self (@Autowired OrderService self) or restructure into separate bean.
Technical Implementation
AOP — Log and Time Every @Service Method
@Aspect
@Component
public class ServiceTimingAspect {
private static final Logger log = LoggerFactory.getLogger(ServiceTimingAspect.class);
// Pointcut: all methods in classes annotated @Service
@Pointcut("within(@org.springframework.stereotype.Service *)")
public void serviceLayer() {}
@Around("serviceLayer()")
public Object logAndTime(ProceedingJoinPoint pjp) throws Throwable {
String method = pjp.getSignature().toShortString();
long start = System.currentTimeMillis();
try {
Object result = pjp.proceed(); // invoke target method
log.debug("{} completed in {}ms", method, System.currentTimeMillis() - start);
return result;
} catch (Exception ex) {
log.error("{} failed after {}ms: {}", method, System.currentTimeMillis() - start, ex.getMessage());
throw ex;
}
}
}
@Transactional Propagation Patterns
@Service
public class OrderService {
@Autowired private OrderRepo orderRepo;
@Autowired private AuditService auditService;
// REQUIRED (default): join existing tx or create new one
@Transactional
public Order createOrder(OrderRequest req) {
Order order = orderRepo.save(new Order(req));
auditService.logCreation(order.getId()); // joins this transaction
return order;
}
// REQUIRES_NEW: always new transaction, suspends outer
// Use for: audit logs that must persist even if outer tx rolls back
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAlways(String event) {
auditRepo.save(new AuditLog(event, Instant.now()));
// committed even if caller's transaction rolls back
}
// SUPPORTS: participates if tx exists, runs without if not
// Use for: read-only queries called from both transactional and non-transactional context
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public List<Order> findAll() {
return orderRepo.findAll();
}
// Rollback only on specific exceptions (default: RuntimeException)
@Transactional(rollbackFor = { PaymentException.class, InventoryException.class })
public void fulfillOrder(Long orderId) throws PaymentException {
// ...
}
// NEVER: throws if called within an active transaction
@Transactional(propagation = Propagation.NEVER)
public Report generateHeavyReport() {
// Enforce this is not called in a tx (would hold tx open too long)
return reportService.build();
}
}
Spring Security Custom JWT Filter
// Register before UsernamePasswordAuthenticationFilter in the filter chain
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Autowired private JwtService jwtService;
@Autowired private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
FilterChain chain) throws ServletException, IOException {
String header = req.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
chain.doFilter(req, res);
return;
}
try {
String token = header.substring(7);
String username = jwtService.extractUsername(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(token, user)) {
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
} catch (JwtException e) {
res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
chain.doFilter(req, res);
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtFilter) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**", "/actuator/health").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
Interview Preparation
Q: Why doesn't @Transactional work when you call a method within the same class?
Spring @Transactional works by creating a CGLIB proxy subclass of your bean. When external code calls orderService.createOrder(), it goes through the proxy, which opens the transaction. When you call this.createOrder() from within the same class, you're calling the real object directly — the proxy is bypassed, no transaction starts. The three fixes: (1) Refactor the method into a separate Spring bean so it's invoked via proxy. (2) Self-inject the bean via @Autowired private OrderService self and call self.createOrder() through the proxy. (3) Use AopContext.currentProxy() to get the proxy (requires @EnableAspectJAutoProxy(exposeProxy=true)).
Q: How does Spring Boot auto-configuration work?
@EnableAutoConfiguration (included in @SpringBootApplication) triggers AutoConfigurationImportSelector, which reads /META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports from every jar on the classpath. Each listed class is annotated with @Conditional* annotations: @ConditionalOnClass (is a library present?), @ConditionalOnMissingBean (did the user define their own?), @ConditionalOnProperty. Only beans whose conditions pass get registered. You can see what was configured and why with --debug flag (condition evaluation report in logs).
Q: What is the difference between @Component, @Service, @Repository, and @Controller?
Functionally, all four trigger component scanning and register a bean. The difference is semantic metadata and added behavior: @Repository adds exception translation (Spring translates vendor-specific SQLException into DataAccessException). @Controller marks the class as a Spring MVC handler, enabling @RequestMapping. @Service and @Component have no added behavior — @Service is a stereotype conveying "business logic layer." Using the right annotation communicates intent and allows future framework enhancements or AOP pointcuts to target the specific layer.
Q: CGLIB proxy vs JDK dynamic proxy — when does Spring use each?
JDK dynamic proxy: only works if the target class implements an interface. Creates a proxy implementing the same interface(s). Used by Spring when proxyTargetClass=false (the default for interface-based injection). CGLIB proxy: works on any class (no interface needed) by generating a subclass at runtime. Used when injecting by class type (not interface) or when @EnableAspectJAutoProxy(proxyTargetClass=true) is set. Spring Boot defaults to CGLIB for @Transactional since Spring 4.x because it's more flexible. CGLIB limitation: cannot proxy final classes or final methods.
Learning Resources
- Spring Framework Reference — Core Container
- Spring Boot Auto-configuration
- Spring in Action — Craig Walls — practical guide
- Baeldung Spring Tutorials — deep-dive articles