Spring Core
Fundamentals

Most developers start with Spring Boot annotations and never understand what's underneath. This section tears Spring open — understanding the container, the IoC principle, bean lifecycle, AOP proxies, and how Spring manages every object in your application from first principles.

Inversion of Control — The Philosophy

Before Spring, a Java class that needed a database connection would create it: new DatabaseConnection(). The class was in control of its own dependencies. This caused hard coupling, untestable code, and configuration scattered across hundreds of classes.

Inversion of Control flips this. Instead of classes creating their dependencies, an external container creates them and hands them to the class. The class no longer controls object creation — the framework does.

Before IoC vs After IoC Java
// ❌ BEFORE IoC — tightly coupled, hard to test
public class OrderService {
    private UserRepository userRepo    = new UserRepositoryImpl();       // hard-coded!
    private EmailService  emailService = new SmtpEmailService("smtp.gmail.com", 587);
    private PaymentGateway payment     = new StripeGateway("sk_prod_..."); // secret in code!

    public void placeOrder(Order order) {
        // Works, but:
        // ✗ Can't swap UserRepository for a mock in tests
        // ✗ Can't change email provider without recompiling
        // ✗ Secret keys embedded in source code
        // ✗ Can't run without real SMTP and Stripe
    }
}

// ✅ AFTER IoC — loose coupling, testable, configurable
public class OrderService {
    private final UserRepository   userRepo;
    private final EmailService     emailService;
    private final PaymentGateway   paymentGateway;

    public OrderService(UserRepository userRepo,
                        EmailService emailService,
                        PaymentGateway paymentGateway) {
        this.userRepo       = userRepo;
        this.emailService   = emailService;
        this.paymentGateway = paymentGateway;
    }
    // ✓ In tests: inject MockUserRepository, FakeEmailService
    // ✓ In prod:  inject JpaUserRepository, SmtpEmailService
    // ✓ Config in one place
    // ✓ Swap Stripe for PayPal by changing ONE config line
}
💡
Hollywood Principle

IoC is sometimes called the "Hollywood Principle": "Don't call us, we'll call you." Your classes don't reach out to get dependencies — the framework delivers them.

IoC is not Spring-specific. It's a design principle. Spring is simply the most popular implementation of IoC in the Java world. The container that implements IoC is called a DI container (or IoC container). Spring's ApplicationContext is that container.

🏭
Why Companies Standardize on IoC

In large codebases with 500+ classes, manually wiring dependencies leads to "main method spaghetti." IoC centralizes all wiring in configuration, makes swapping implementations trivial (production DB vs test DB, real payment vs mock), and enables non-invasive cross-cutting concerns like logging, transactions, and security checks — all without touching business logic.

Dependency Injection — How It Works

Dependency Injection is the most common implementation of IoC. Spring reads your classes, figures out what they need, creates those things, and injects them. There are three injection styles — each with different tradeoffs that matter in real teams.

Recommended — Use This Always for Required Dependencies

Constructor injection is the only style Spring and the community recommend for mandatory dependencies. It makes dependencies explicit, enables immutability (final fields), and makes the class testable without Spring at all.

Constructor Injection — The Right Way Java
@Service
public class OrderService {
    // final = immutable after construction = thread-safe by design
    private final OrderRepository      orderRepository;
    private final PaymentService       paymentService;
    private final NotificationService  notificationService;

    // @Autowired is OPTIONAL when there's only ONE constructor (Spring 4.3+)
    public OrderService(OrderRepository orderRepository,
                        PaymentService paymentService,
                        NotificationService notificationService) {
        Objects.requireNonNull(orderRepository, "OrderRepository must not be null");
        this.orderRepository     = orderRepository;
        this.paymentService      = paymentService;
        this.notificationService = notificationService;
    }
    // Benefits:
    // ✓ All deps in constructor signature — instantly visible
    // ✓ Fields are final — no accidental mutation
    // ✓ Fails FAST at startup if a dep is missing (not at runtime)
    // ✓ Testable without Spring — just call new OrderService(mock, mock, mock)
}

// TESTING without Spring at all:
class OrderServiceTest {
    @Test void shouldPlaceOrder() {
        OrderService svc = new OrderService(
            mock(OrderRepository.class),
            mock(PaymentService.class),
            mock(NotificationService.class)
        );
        // Test pure Java, no Spring context needed
    }
}
🚨
Avoid — Common But Problematic

Field injection is the most-seen Spring code, but also the worst choice. It hides dependencies, prevents final fields, and makes the class impossible to test without Spring. Many engineering teams ban it in code reviews.

Field Injection — Why It's Problematic Java
@Service
public class OrderService {
    @Autowired // Spring uses reflection to set this — bypasses normal Java
    private OrderRepository orderRepository; // NOT final — mutable!

    @Autowired
    private PaymentService paymentService; // Hidden dependency — not in constructor

    // PROBLEMS:
    // ✗ Can't make fields final
    // ✗ To test: need a Spring context OR ugly reflection hacks
    // ✗ new OrderService() — all fields are null (NPE waiting to happen)
    // ✗ Easy to add 10+ dependencies without noticing — class gets too large
    // ✗ Circular dependency detection is harder
}

// To test field injection — messy:
class OrderServiceTest {
    @Test void shouldWork() {
        OrderService svc = new OrderService();
        ReflectionTestUtils.setField(svc, "orderRepository", mock(OrderRepository.class));
        // ... every test needs this boilerplate
    }
}
⚠️
Use for Optional Dependencies Only

Setter injection is appropriate when a dependency is truly optional (has a sensible default). For required dependencies, always use constructors.

Setter Injection — For Optional Deps Java
@Service
public class NotificationService {
    private final EmailService emailService; // required — constructor injection

    private SmsService smsService; // optional — setter injection

    public NotificationService(EmailService emailService) {
        this.emailService = emailService;
    }

    // @Autowired(required=false) — Spring injects if the bean exists, skips if not
    @Autowired(required = false)
    public void setSmsService(SmsService smsService) {
        this.smsService = smsService;
    }

    public void notify(String userId, String message) {
        emailService.send(userId, message);        // always available
        if (smsService != null) {                  // guard before use
            smsService.send(userId, message);
        }
    }
}

The Spring Bean Container

The ApplicationContext is Spring's bean container — the heart of every Spring application. It creates beans, injects dependencies, manages their lifecycle, and provides them on demand. Understanding it removes the "Spring magic" mystery.

The ApplicationContext is not just a registry of objects. It's a fully-featured runtime environment — with event publishing, internationalization, resource loading, and a complete lifecycle management system.

Spring Bean Container — Interactive Explorer

Explore the ApplicationContext. Click any bean to inspect its type, scope, and dependency chain. Simulate startup to see initialization order.

ApplicationContext vs BeanFactory

Spring has two container types. You'll almost always use ApplicationContext, but understanding the difference matters for interviews and internals.

FeatureBeanFactoryApplicationContext
Bean instantiationLazy (on first request)Eager (at startup) for singletons
Annotation support❌ Manual only✅ Full annotation processing
Event system❌ None✅ ApplicationEventPublisher
Internationalization❌ None✅ MessageSource
AOP integrationLimited✅ Full AOP support
BeanPostProcessorsManual registration✅ Auto-detected
Used in Spring Boot?❌ No✅ Always
ApplicationContext — Types and What Spring Boot Uses Java
// Spring Boot creates an ApplicationContext automatically.
// But understanding the types helps you reason about startup.

// Type 1: AnnotationConfigApplicationContext
// Used by: Pure Spring (no web), Spring Boot non-web apps
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
OrderService svc = ctx.getBean(OrderService.class);

// Type 2: AnnotationConfigServletWebServerApplicationContext
// Used by: Spring Boot web apps (embedded Tomcat)
// Spring Boot picks this automatically when spring-webmvc is on classpath

// Type 3: AnnotationConfigReactiveWebServerApplicationContext
// Used by: Spring WebFlux apps (reactive, non-blocking)
// Spring Boot picks this when spring-webflux is on classpath

// In practice, you rarely instantiate these directly.
// Spring Boot's SpringApplication.run() does it based on classpath detection.

// How Spring Boot detects which to use:
// 1. Scan classpath for spring-webmvc → use Servlet context
// 2. Scan classpath for spring-webflux (and no webmvc) → use Reactive context
// 3. Neither → use plain AnnotationConfigApplicationContext

// What does the ApplicationContext actually store?
// BeanDefinition — metadata about each bean (class, scope, init/destroy methods, deps)
BeanFactory factory = (BeanFactory) ctx;
BeanDefinition def = ((BeanDefinitionRegistry) factory).getBeanDefinition("orderService");
System.out.println(def.getBeanClassName()); // "com.myapp.OrderService"
System.out.println(def.getScope());         // "singleton"
System.out.println(def.isLazyInit());       // false
⚠️
Singleton ≠ Thread-Safe

Spring's singleton scope means one instance per container, not per thread. If your singleton bean stores state in instance variables, multiple threads will race on that state. Always make singleton beans stateless, or use thread-local storage / synchronized access.

Bean Lifecycle — All 8 Phases

Every Spring bean goes through a precise lifecycle from the moment the context starts to the moment it shuts down. Understanding this lifecycle is critical for debugging startup failures, resource leaks, and understanding how AOP works.

🏭
Why This Matters in Production

A common production bug: a bean tries to use an @Autowired dependency in its constructor — but at construction time, dependencies haven't been injected yet. The result is an NPE at startup. Another: failing to implement @PreDestroy means database connections, thread pools, and file handles are never properly closed on graceful shutdown.

Bean Lifecycle Phases — Interactive Walkthrough

Click each phase or use "Walk Through" to see exactly what Spring does at every stage of a bean's life.

Full Bean Lifecycle in Code — Every Hook Java
@Service
public class CacheService implements BeanNameAware, ApplicationContextAware,
                                     InitializingBean, DisposableBean {

    private String beanName;
    private ApplicationContext context;
    private final Map<String, Object> cache = new ConcurrentHashMap<>();

    // ──── PHASE 1: Instantiation ────────────────────────────────────────
    // Spring calls the constructor (via reflection).
    // @Autowired fields and setters are NOT yet injected here.
    public CacheService() {
        System.out.println("1. Constructor called — deps NOT available yet");
        // DON'T call this.metricsService.record() here — it's null!
    }

    // ──── PHASE 2: Dependency Injection ─────────────────────────────────
    // Spring injects @Autowired fields/setters after construction.
    @Autowired
    private MetricsService metricsService;  // Available AFTER constructor

    // ──── PHASE 3: Aware Interfaces ──────────────────────────────────────
    // Spring calls Aware callbacks to give the bean container info.
    @Override
    public void setBeanName(String name) {
        this.beanName = name; // "cacheService"
        System.out.println("3. BeanNameAware: " + name);
    }

    @Override
    public void setApplicationContext(ApplicationContext ctx) {
        this.context = ctx;
        System.out.println("3. ApplicationContextAware: context injected");
    }

    // ──── PHASE 4-5: BeanPostProcessors ──────────────────────────────────
    // Spring-managed, not in your code.
    // This is where AOP proxies are created, @Transactional is wired, etc.
    // BeanPostProcessor.postProcessBeforeInitialization() runs at Phase 4
    // BeanPostProcessor.postProcessAfterInitialization() runs at Phase 5

    // ──── PHASE 6: Initialization Callbacks ──────────────────────────────
    // @PostConstruct runs first — PREFER this (no Spring interface required)
    @PostConstruct
    public void init() {
        System.out.println("6a. @PostConstruct — all deps ready, safe to use them");
        warmUpCache();    // metricsService is injected — safe to call
    }

    // InitializingBean.afterPropertiesSet() runs second
    @Override
    public void afterPropertiesSet() {
        System.out.println("6b. afterPropertiesSet — runs after @PostConstruct");
    }

    // init-method from @Bean(initMethod="setup") runs third (if configured)

    // ──── PHASE 7: Bean in Use ───────────────────────────────────────────
    // Bean is serving requests. This is the normal operational phase.

    // ──── PHASE 8: Destruction Callbacks ─────────────────────────────────
    // @PreDestroy runs first — PREFER this
    @PreDestroy
    public void cleanup() {
        System.out.println("8a. @PreDestroy — context shutting down");
        cache.clear();           // release memory
        metricsService.flush();  // flush metrics before exit
    }

    // DisposableBean.destroy() runs second
    @Override
    public void destroy() {
        System.out.println("8b. DisposableBean.destroy() — final cleanup");
    }

    private void warmUpCache() {
        // Pre-load frequently-accessed data at startup
        // Users get fast first-request latency instead of cold-start lag
    }
}

// STARTUP OUTPUT ORDER:
// 1. Constructor called — deps NOT available yet
// 3. BeanNameAware: cacheService
// 3. ApplicationContextAware: context injected
// 6a. @PostConstruct — all deps ready, safe to use them
// 6b. afterPropertiesSet — runs after @PostConstruct
// ... (bean serves requests) ...
// 8a. @PreDestroy — context shutting down
// 8b. DisposableBean.destroy() — final cleanup

BeanPostProcessors — How Spring Adds Magic

BeanPostProcessors are the mechanism behind all Spring "magic" — @Transactional, @Async, @Cacheable, @Validated. They intercept every bean before and after initialization to wrap it in a proxy or modify its behavior.

Custom BeanPostProcessor — Internals Exposed Java
// A BeanPostProcessor runs for EVERY bean in the context.
// This is how Spring's internal @Transactional processing works:
@Component
public class TransactionProxyPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        // Check if any method has @Transactional
        boolean hasTransactional = Arrays.stream(bean.getClass().getMethods())
            .anyMatch(m -> m.isAnnotationPresent(Transactional.class));

        if (hasTransactional) {
            // Return a PROXY wrapping the real bean
            // The proxy intercepts calls and adds transaction logic
            return Proxy.newProxyInstance(
                bean.getClass().getClassLoader(),
                bean.getClass().getInterfaces(),
                (proxy, method, args) -> {
                    if (method.isAnnotationPresent(Transactional.class)) {
                        // Begin transaction, call method, commit/rollback
                        return executeInTransaction(bean, method, args);
                    }
                    return method.invoke(bean, args); // passthrough
                }
            );
        }
        return bean; // no change
    }
}

// Spring's real AutoProxyCreator does exactly this — much more sophisticated,
// but the fundamental pattern is identical.

Bean Scopes

By default, every Spring bean is a singleton — one instance shared across the entire application. But this isn't always the right choice. Spring provides five scopes, each solving a different problem.

ScopeInstancesLifecycleUse WhenThread Safe?
singleton1 per containerContainer lifetimeStateless services, repositories (almost always)⚠️ Only if stateless
prototypeNew per getBean()Caller managesStateful beans (rare in web apps)✅ Yes
request1 per HTTP requestHTTP requestPer-request context (requestId, userId)✅ Yes
session1 per HTTP sessionHTTP sessionShopping cart, user preferences⚠️ One user, many requests
application1 per ServletContextApp lifetimeSame as singleton in most cases⚠️ Only if stateless
Bean Scopes — Real Usage Patterns Java
// SINGLETON — default, shared instance
@Service  // = @Service + @Scope("singleton")
public class OrderService { /* stateless — no instance vars */ }

// PROTOTYPE — new instance every time it's injected or requested
@Component
@Scope("prototype")
public class ReportGenerator {
    private final List<String> lines = new ArrayList<>(); // safe — fresh list each time
}

// REQUEST scope — one instance per HTTP request (web apps only)
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
// ↑ proxyMode REQUIRED when injecting short-lived scopes into singletons
public class RequestContext {
    private final String requestId = UUID.randomUUID().toString();
    private String userId; // populated from JWT at request start
    // Use this to pass per-request context without ThreadLocal
}

// ─────────────────────────────────────────────────────────
// CLASSIC TRAP: Injecting prototype into singleton
@Service // singleton — created ONCE
public class ReportService {

    // ❌ WRONG: This prototype is injected ONCE at startup.
    // The singleton holds ONE ReportGenerator forever — prototype is meaningless!
    @Autowired
    private ReportGenerator generator; // Same instance every call!

    // ✅ RIGHT: Use ObjectFactory or ApplicationContext to get fresh instances
    @Autowired
    private ObjectFactory<ReportGenerator> generatorFactory;

    public Report generate(ReportRequest req) {
        ReportGenerator gen = generatorFactory.getObject(); // NEW instance each call
        return gen.build(req);
    }
}

// ─────────────────────────────────────────────────────────
// Why proxyMode is needed for request/session scope:
// A singleton OrderService is created ONCE at startup.
// But RequestContext only exists during a request — not at startup!
// Spring injects a PROXY of RequestContext at startup.
// The proxy forwards calls to the REAL RequestContext for the current request.
@Service
public class OrderService {
    @Autowired
    private RequestContext requestCtx; // This is actually a PROXY at startup time

    public void placeOrder() {
        String userId = requestCtx.getUserId(); // Proxy delegates to real request-scoped bean
    }
}

Circular Dependencies — Detection & Resolution

A circular dependency occurs when Bean A needs Bean B, and Bean B also needs Bean A. Spring handles this differently depending on the injection type — and getting it wrong causes subtle runtime bugs or startup failures.

Circular Dependency Simulator

Simulate circular dependency scenarios and see exactly how Spring detects and handles them.

Circular Dependencies — The Three Scenarios Java
// ──────────────────────────────────────────────────────────────────────────
// SCENARIO 1: Constructor Injection → Fails at startup (GOOD — fail fast)
// ──────────────────────────────────────────────────────────────────────────
@Service
public class ServiceA {
    public ServiceA(ServiceB b) { } // Needs ServiceB
}
@Service
public class ServiceB {
    public ServiceB(ServiceA a) { } // Needs ServiceA
}
// Spring tries to create ServiceA → needs ServiceB → needs ServiceA → LOOP!
// Error: The dependencies of some of the beans in the application context
//        form a cycle: serviceA → serviceB → serviceA
// Spring 6+ detects this at startup. Spring Boot 2.6+ requires explicit opt-in
// to allow circular deps (spring.main.allow-circular-references=true)

// ──────────────────────────────────────────────────────────────────────────
// SCENARIO 2: Field/Setter Injection → Silently "works" (DANGEROUS)
// ──────────────────────────────────────────────────────────────────────────
@Service
public class ServiceA {
    @Autowired ServiceB b; // Field injection — Spring injects AFTER constructor
}
@Service
public class ServiceB {
    @Autowired ServiceA a; // Field injection
}
// Spring CAN resolve this:
// 1. Create ServiceA (constructor — b is null)
// 2. Create ServiceB (constructor — a is null)
// 3. Inject ServiceB.a = ServiceA instance (half-initialized!)
// 4. Inject ServiceA.b = ServiceB instance
// ⚠️ Works but dangerous: each bean briefly exists in a half-initialized state.
// In complex scenarios this causes subtle bugs that are hard to debug.

// ──────────────────────────────────────────────────────────────────────────
// SCENARIO 3: @Lazy — The Proper Solution for Unavoidable Circular Deps
// ──────────────────────────────────────────────────────────────────────────
@Service
public class ServiceA {
    private final ServiceB b;
    public ServiceA(@Lazy ServiceB b) { // Spring injects a PROXY of B at construction
        this.b = b;                     // Real ServiceB is resolved on first method call
    }
}
@Service
public class ServiceB {
    private final ServiceA a;
    public ServiceB(ServiceA a) { this.a = a; }
}
// @Lazy on the constructor param tells Spring: inject a proxy placeholder for B.
// The real ServiceB is created and wired when b is first called.

// ──────────────────────────────────────────────────────────────────────────
// BETTER SOLUTION: Redesign to remove the circular dependency
// ──────────────────────────────────────────────────────────────────────────
// If A needs B and B needs A, you have a design problem.
// Extract the shared behavior into a third service C.
@Service
public class ServiceC {
    // Contains the shared logic both A and B need
}
@Service
public class ServiceA {
    public ServiceA(ServiceC c, ServiceB b) { } // No circular dep
}
@Service
public class ServiceB {
    public ServiceB(ServiceC c) { } // No circular dep
}
🚨
Spring Boot 2.6+ Changed the Default

Before Spring Boot 2.6, circular dependencies were silently allowed via field injection. From 2.6 onward, they're forbidden by default. This is a good thing — it forces better design. If you're migrating a legacy app and suddenly see "The dependencies form a cycle" errors, that's Spring surfacing previously hidden design problems. Fix them properly rather than setting spring.main.allow-circular-references=true.

Autowiring, @Qualifier & @Primary

When Spring sees @Autowired, it searches the container for a matching bean. For simple cases this just works. But when multiple beans of the same type exist, Spring needs help deciding which one to inject.

@Qualifier, @Primary, and Named Injection Java
// Multiple implementations of the same interface:
public interface MessageSender { void send(String msg); }

@Component("emailSender")
public class EmailMessageSender implements MessageSender { ... }

@Component("smsSender")
public class SmsMessageSender implements MessageSender { ... }

@Component("pushSender")
@Primary // ← Makes this the DEFAULT when @Autowired without @Qualifier
public class PushMessageSender implements MessageSender { ... }

// ─────────────────────────────────────────────────────────
// CASE 1: Just @Autowired — Spring picks the @Primary bean
@Service
public class NotificationService {
    @Autowired
    private MessageSender sender; // Gets PushMessageSender (it's @Primary)
}

// CASE 2: @Qualifier — explicitly choose a specific bean
@Service
public class EmailService {
    private final MessageSender emailSender;

    public EmailService(@Qualifier("emailSender") MessageSender emailSender) {
        this.emailSender = emailSender; // Gets EmailMessageSender
    }
}

// CASE 3: Inject ALL beans of a type into a List
@Service
public class MultiChannelNotificationService {
    private final List<MessageSender> senders;

    public MultiChannelNotificationService(List<MessageSender> senders) {
        this.senders = senders; // Gets [EmailSender, SmsSender, PushSender]
    }

    public void broadcast(String msg) {
        senders.forEach(s -> s.send(msg)); // Sends via all channels
    }
}

// CASE 4: Inject into a Map (key = bean name)
@Autowired
private Map<String, MessageSender> senderMap;
// senderMap = {"emailSender": ..., "smsSender": ..., "pushSender": ...}

// Useful for strategy pattern:
public void sendVia(String channel, String msg) {
    senderMap.get(channel + "Sender").send(msg); // Dynamic dispatch
}

// ─────────────────────────────────────────────────────────
// Custom @Qualifier — type-safe disambiguation
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface ForEmail {}

@Component @ForEmail
public class EmailMessageSender implements MessageSender { ... }

@Service
public class EmailService {
    public EmailService(@ForEmail MessageSender sender) { ... }
    // Much more expressive than @Qualifier("emailSender") (stringly typed)
}

Component Scanning

Component scanning is how Spring discovers beans automatically. Instead of manually registering every class, Spring scans packages and registers any class annotated with a stereotype annotation.

Component Scanning — How It Works Internally Java
// Stereotype annotations — all are specializations of @Component
// @Component  → generic Spring-managed component
// @Service    → business logic layer (semantics only — no extra behavior)
// @Repository → data access layer (adds automatic exception translation)
// @Controller / @RestController → web layer
// @Configuration → bean factory class

// Spring Boot enables component scanning from the main class package:
@SpringBootApplication  // Includes @ComponentScan
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
// This scans com.myapp.** — any class in this package or subpackage
// that has @Component (or any stereotype) is registered as a bean.

// Customize scanning:
@SpringBootApplication
@ComponentScan(
    basePackages = {"com.myapp", "com.shared.lib"},
    excludeFilters = @ComponentScan.Filter(
        type = FilterType.ANNOTATION,
        classes = LoadTestOnly.class // Exclude @LoadTestOnly beans in production
    )
)
public class Application { }

// How Spring processes @ComponentScan internally:
// 1. ClassPathScanningCandidateComponentProvider scans the classpath
// 2. For each .class file, it reads bytecode (not loading the class yet!)
// 3. Checks for @Component meta-annotation (via ASM bytecode reader)
// 4. Creates a BeanDefinition for each candidate
// 5. BeanDefinitionRegistry registers the BeanDefinition
// 6. Later: BeanFactory instantiates beans from BeanDefinitions

// This bytecode-first approach means Spring can filter classes before loading
// them into the JVM — important for performance in large codebases.
💡
Startup Performance Tip

In large applications with hundreds of packages, broad component scanning (scanning com.**) slows startup. Spring Boot 2.7+ introduced "Spring AOT" and lazy initialization support. For production, consider spring.main.lazy-initialization=true during development to speed up restarts, and explicit base packages to limit scan scope.

@Configuration & @Bean

@Configuration classes are Java-based bean factories. They give you full Java control over bean creation — useful for third-party libraries (which you can't annotate), conditional bean creation, and complex wiring logic that annotations can't express.

@Configuration — Full Power Java
@Configuration
public class AppConfig {

    // ── Basic @Bean — register a third-party object ──────────────────────
    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }

    // ── @Bean with dependencies — inject other beans as params ───────────
    @Bean
    public UserRepository userRepository(DataSource dataSource) {
        // Spring auto-injects the DataSource bean as a parameter
        return new JdbcUserRepository(dataSource);
    }

    // ── @Bean with init/destroy lifecycle ────────────────────────────────
    @Bean(initMethod = "connect", destroyMethod = "disconnect")
    public RedisClient redisClient(@Value("${redis.host}") String host) {
        return new RedisClient(host, 6379);
    }

    // ── Conditional beans — only created when condition is met ───────────
    @Bean
    @Profile("production")           // Only in production profile
    public StripePaymentGateway stripeGateway(@Value("${stripe.key}") String key) {
        return new StripePaymentGateway(key);
    }

    @Bean
    @Profile("test")                 // Only in test profile
    public FakePaymentGateway fakeGateway() {
        return new FakePaymentGateway(); // No real charges
    }

    @Bean
    @ConditionalOnProperty(name = "feature.analytics.enabled", havingValue = "true")
    public AnalyticsService analyticsService() {
        return new AnalyticsService();   // Only created if feature flag is on
    }

    // ── Method calls between @Bean methods — the CGLIB proxy trick ───────
    @Bean
    public ServiceA serviceA() {
        return new ServiceA(sharedDependency()); // calls sharedDependency()
    }

    @Bean
    public ServiceB serviceB() {
        return new ServiceB(sharedDependency()); // also calls sharedDependency()
    }

    @Bean
    public SharedDependency sharedDependency() {
        return new SharedDependency(); // Returns the SAME SINGLETON instance both times
        // Why? @Configuration classes are CGLIB-proxied.
        // Calls to @Bean methods return the cached bean — not a new instance.
        // If AppConfig were @Component instead, each call would return a NEW object!
    }
}

// ── @Configuration vs @Component for factory methods ──────────────────────
// @Configuration (CGLIB proxy) → @Bean methods return singleton (correct)
// @Component (no proxy)        → @Bean methods create new instances (usually wrong)
//
// Use @Configuration for configuration classes with inter-bean dependencies.
// Use @Component only for simple beans without @Bean methods.

Spring's Internal Use of Reflection

Every time Spring creates a bean, processes an annotation, or performs dependency injection, it uses Java Reflection. Understanding this explains Spring's startup cost, why final classes can break certain features, and how GraalVM Native Image optimization works.

What Spring Does with Reflection — Under the Hood Java
// Spring uses reflection for EVERY step of bean creation.
// Here's a simplified version of what happens when you write @Service:

// YOUR CODE:
@Service
public class OrderService {
    @Autowired
    private OrderRepository repository;

    @Value("${app.maxOrders}")
    private int maxOrders;
}

// SPRING INTERNALLY (simplified):
class SpringContainer {

    Object createBean(Class<?> clazz) throws Exception {
        // Step 1: Read class annotations
        Service serviceAnnotation = clazz.getAnnotation(Service.class);
        if (serviceAnnotation == null) return null; // not a bean

        // Step 2: Find constructor and create instance
        Constructor<?> constructor = clazz.getDeclaredConstructor(); // no-arg
        constructor.setAccessible(true); // bypasses access modifiers!
        Object bean = constructor.newInstance();

        // Step 3: Process @Autowired fields
        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(Autowired.class)) {
                field.setAccessible(true); // bypasses private!
                Object dep = getBean(field.getType()); // find dep in container
                field.set(bean, dep); // inject it
            }
        }

        // Step 4: Process @Value fields
        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(Value.class)) {
                String expression = field.getAnnotation(Value.class).value();
                Object value = resolveExpression(expression); // ${app.maxOrders}
                field.setAccessible(true);
                field.set(bean, value);
            }
        }

        // Step 5: Call @PostConstruct methods
        for (Method method : clazz.getMethods()) {
            if (method.isAnnotationPresent(PostConstruct.class)) {
                method.setAccessible(true);
                method.invoke(bean);
            }
        }

        return bean;
    }
}

// ─────────────────────────────────────────────────────────
// REFLECTION PERFORMANCE IMPLICATIONS:
// - First call to getDeclaredFields() / getDeclaredMethods() is SLOW
// - Spring caches reflection results aggressively (ConcurrentHashMap)
// - This is why Spring startup cost scales with NUMBER of beans, not requests
// - At runtime (after startup), reflection cost is minimal — results are cached

// ─────────────────────────────────────────────────────────
// WHY final BREAKS CGLIB PROXIES:
// CGLIB creates a subclass of your bean to add proxy behavior.
// final class = cannot be subclassed = CGLIB cannot proxy it.
@Service
public final class UserService { // ❌ CGLIB cannot create a subclass
    @Transactional
    public User findById(Long id) { ... } // @Transactional IGNORED!
}

// Final METHODS have the same problem:
@Service
public class UserService {
    @Transactional
    public final User findById(Long id) { ... } // CGLIB can't override final methods!
}

// ─────────────────────────────────────────────────────────
// GRAALVM NATIVE IMAGE — Reflection at compile time:
// GraalVM compiles Spring apps to native binaries.
// Problem: native binaries can't do runtime reflection.
// Solution: Spring AOT (Ahead-of-Time) processing generates reflection hints
// at build time, so the native binary "knows" all needed reflection in advance.
// This reduces startup from seconds to milliseconds — at the cost of build time.

Spring Expression Language (SpEL)

SpEL is a powerful expression language embedded in Spring annotations. It lets you inject computed values, reference other beans, evaluate conditions, and perform runtime calculations — all within annotation strings.

SpEL — Practical Usage Patterns Java
@Component
public class AppConfiguration {

    // ── @Value basics ─────────────────────────────────────────────────────
    @Value("${server.port:8080}")          // property with default
    private int serverPort;

    @Value("${app.name}")                  // required property (fails if missing)
    private String appName;

    @Value("#{T(java.lang.Runtime).getRuntime().availableProcessors()}")
    private int availableCpus;            // SpEL: call static method

    @Value("#{systemProperties['os.name']}")
    private String osName;                // Read JVM system property

    @Value("#{@configBean.getMaxRetries() * 2}")
    private int maxRetries;               // Reference another bean (@configBean) and call it

    @Value("#{T(java.util.UUID).randomUUID().toString()}")
    private String instanceId;            // Generate a unique ID at startup

    // ── @Cacheable with SpEL cache key ────────────────────────────────────
    @Cacheable(value = "users", key = "#id + '_' + #region")
    public User findUser(Long id, String region) { ... }
    // Cache key = "42_EU" — different caches per region

    @Cacheable(value = "orders",
               key = "#customer.id",
               condition = "#customer.isPremium()",  // Only cache premium customers
               unless = "#result == null")           // Don't cache null results
    public List<Order> findOrders(Customer customer) { ... }

    // ── @PreAuthorize with SpEL ───────────────────────────────────────────
    @PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
    public void deleteUser(Long userId) { ... }
    // Allow if ADMIN, OR if the user is deleting their own account

    @PreAuthorize("@securityService.canAccess(#resourceId, authentication)")
    public Resource getResource(Long resourceId) { ... }
    // Delegate to a custom security service bean

    // ── Programmatic SpEL (advanced) ─────────────────────────────────────
    public void evaluateDynamically(String expression, Object rootObject) {
        ExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext ctx = new StandardEvaluationContext(rootObject);
        ctx.setVariable("discount", 0.15);
        Double result = parser.parseExpression(expression)
                              .getValue(ctx, Double.class);
    }
}

Aspect-Oriented Programming (AOP)

AOP solves a specific problem: cross-cutting concerns — functionality that cuts across many classes and methods, like logging, transaction management, security checks, and metrics. Without AOP, you'd duplicate this code everywhere.

The Problem AOP Solves

Without AOP vs With AOP Java
// ❌ WITHOUT AOP: Boilerplate duplicated in EVERY service method
public class OrderService {
    public Order placeOrder(OrderRequest req) {
        log.info("→ placeOrder: {}", req);                           // duplicated
        long start = System.currentTimeMillis();                     // duplicated
        Transaction tx = txManager.begin();                          // duplicated
        try {
            Order order = processOrder(req);
            tx.commit();                                             // duplicated
            metrics.counter("orders.placed").increment();            // duplicated
            log.info("← placeOrder [{}ms]", System.currentTimeMillis()-start);
            return order;
        } catch (Exception e) {
            tx.rollback();                                           // duplicated
            log.error("✗ placeOrder failed", e);                    // duplicated
            throw e;
        }
    }
    // Repeat this boilerplate for EVERY method in EVERY service...
}

// ✅ WITH AOP: Your business logic, clean and focused
@Service @Transactional
public class OrderService {
    public Order placeOrder(OrderRequest req) {
        return processOrder(req); // Just business logic. AOP handles the rest.
    }
}

// ONE aspect handles logging for ALL services:
@Aspect @Component
public class LoggingAspect {
    @Around("execution(* com.myapp.services.*.*(..))")
    public Object log(ProceedingJoinPoint pjp) throws Throwable {
        String method = pjp.getSignature().toShortString();
        long start = System.currentTimeMillis();
        log.info("→ {}", method);
        try {
            Object result = pjp.proceed();
            log.info("← {} [{}ms]", method, System.currentTimeMillis()-start);
            return result;
        } catch (Throwable ex) {
            log.error("✗ {} failed: {}", method, ex.getMessage());
            throw ex;
        }
    }
}

AOP Advice Types — All Five

All Five Advice Types with Real Use Cases Java
@Aspect @Component
public class EnterpriseAspect {

    // 1. @Before — runs BEFORE the method
    //    Cannot stop execution (unless it throws). Can read args.
    @Before("@annotation(com.myapp.Audited)")
    public void auditAccess(JoinPoint jp) {
        String user = SecurityContextHolder.getContext().getAuthentication().getName();
        auditLog.write(user, jp.getSignature().getName(), jp.getArgs());
    }

    // 2. @AfterReturning — runs only if method RETURNS NORMALLY (no exception)
    @AfterReturning(pointcut = "execution(* com.myapp.services.OrderService.place(..))",
                    returning = "result")
    public void onOrderSuccess(Object result) {
        Order order = (Order) result;
        metrics.counter("orders.placed").increment();
    }

    // 3. @AfterThrowing — runs only if method THROWS an exception
    @AfterThrowing(pointcut = "execution(* com.myapp.services.*.*(..))",
                   throwing = "ex")
    public void onServiceError(JoinPoint jp, Exception ex) {
        metrics.counter("service.errors",
                        "class", jp.getTarget().getClass().getSimpleName(),
                        "method", jp.getSignature().getName()).increment();
        alerting.notify("Service failure: " + jp.getSignature() + " — " + ex.getMessage());
    }

    // 4. @After — runs ALWAYS (like finally), regardless of outcome
    @After("execution(* com.myapp.services.*.*(..))")
    public void cleanupContext(JoinPoint jp) {
        MDC.remove("requestId"); // Always clean up MDC context
    }

    // 5. @Around — most powerful: full control of execution
    //    Can prevent execution, modify args, change return value
    @Around("@annotation(rateLimited)")
    public Object enforceRateLimit(ProceedingJoinPoint pjp,
                                   RateLimited rateLimited) throws Throwable {
        String key = getUserId() + ":" + pjp.getSignature().getName();
        if (!rateLimiter.tryAcquire(key, rateLimited.requestsPerMinute())) {
            throw new TooManyRequestsException("Rate limit exceeded");
        }
        return pjp.proceed(); // Continue with the actual method call
    }
}

// ─────────────────────────────────────────────────────────
// POINTCUT EXPRESSIONS — The targeting language:
// execution(* com.myapp.service.*.*(..))  — any method in service package
// @annotation(Transactional)              — methods with @Transactional
// @within(Repository)                     — all methods on @Repository classes
// bean(orderService)                      — methods on the "orderService" bean
// args(Long, ..)                          — methods where first arg is Long
// within(com.myapp..*) && !@annotation(NoAOP) — combine with &&, ||, !

// ─────────────────────────────────────────────────────────
// REUSABLE POINTCUTS — avoid repetition:
@Aspect @Component
public class PointcutLibrary {
    @Pointcut("execution(* com.myapp.services..*(..))")
    public void serviceLayer() {}

    @Pointcut("execution(* com.myapp.controllers..*(..))")
    public void controllerLayer() {}

    @Pointcut("serviceLayer() || controllerLayer()")
    public void applicationLayer() {}
}
// Now reuse them:
@Around("PointcutLibrary.applicationLayer()")
public Object trace(ProceedingJoinPoint pjp) throws Throwable { ... }

Dynamic Proxies — The Engine Behind @Transactional

This is the most important "Spring magic" to demystify. Every time you use @Transactional, @Async, @Cacheable, or @PreAuthorize, Spring doesn't modify your class. Instead, it creates a proxy object that wraps your bean and intercepts method calls.

AOP Proxy — Live Execution Tracer

Trace how AOP intercepts a method call through a proxy. Click "Internal Call" to see the self-invocation bug that trips up every Spring developer.

JDK Proxy vs CGLIB — Two Proxy Types Explained Java
// ── JDK Dynamic Proxy: works with INTERFACES ─────────────────────────────
// The proxy implements the same interface as your bean.
public interface UserService { User findById(Long id); }

@Service
public class UserServiceImpl implements UserService {
    @Transactional
    public User findById(Long id) { /* ... */ }
}
// context.getBean(UserService.class)
//   → returns JDK Proxy, NOT UserServiceImpl
//   → instanceof UserService     = true
//   → instanceof UserServiceImpl = false (it's a proxy!)

// ── CGLIB Proxy: works with CONCRETE CLASSES ─────────────────────────────
@Service
public class ProductService { // No interface
    @Transactional
    public Product findById(Long id) { /* ... */ }
}
// Spring generates: class ProductService$$SpringCGLIB$$0 extends ProductService
// Overrides all methods to add transaction logic around them.
// context.getBean(ProductService.class)
//   → instanceof ProductService = true (because CGLIB extends it)

// ── Spring Boot 2.x+ defaults ─────────────────────────────────────────────
// Spring Boot defaults to CGLIB even when interfaces are present.
// Reason: avoids confusion about instanceof checks and cast exceptions.
// To revert to JDK proxies: spring.aop.proxy-target-class=false

// ── THE SELF-INVOCATION BUG — Every developer hits this once ─────────────
@Service
public class OrderService {

    @Transactional  // ← This is on the proxy, which wraps the bean
    public void placeOrder(OrderRequest req) {
        // This works: caller → proxy → real bean's placeOrder()
        // Proxy begins transaction, then calls this method on the real bean

        saveOrder(req);         // ← PROBLEM: this.saveOrder() bypasses the proxy!
                                //   Spring AOP never sees this call.
                                //   @Transactional(REQUIRES_NEW) on saveOrder is IGNORED.
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW) // SILENTLY IGNORED!
    public void saveOrder(OrderRequest req) {
        // Runs in the SAME transaction as placeOrder, not a new one.
        // If placeOrder rolls back, this also rolls back — violating your intent.
    }
}

// ── Three solutions to self-invocation ────────────────────────────────────

// SOLUTION 1: @Lazy self-injection (quick fix)
@Service
public class OrderService {
    @Lazy @Autowired private OrderService self; // Injects the PROXY of self

    @Transactional
    public void placeOrder(OrderRequest req) {
        self.saveOrder(req); // Goes through proxy → REQUIRES_NEW is honored
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveOrder(OrderRequest req) { /* now in own transaction */ }
}

// SOLUTION 2: Extract to separate bean (cleanest architecture)
@Service
public class OrderService {
    private final OrderPersistenceService persistence; // separate bean

    @Transactional
    public void placeOrder(OrderRequest req) {
        persistence.saveOrder(req); // Different bean → proxy is invoked → works
    }
}
@Service
public class OrderPersistenceService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveOrder(OrderRequest req) { /* correctly in own transaction */ }
}

// SOLUTION 3: AopContext.currentProxy() (avoid — tightly couples to Spring AOP)
@Service
public class OrderService {
    @Transactional
    public void placeOrder(OrderRequest req) {
        ((OrderService) AopContext.currentProxy()).saveOrder(req);
    }
}

Spring Event System

Spring's event system lets beans communicate without direct coupling. Instead of serviceA.callMethodInServiceB(), service A publishes an event and any number of listeners can react — without A knowing who's listening. This is one of the most underused features for building clean, decoupled architectures.

Spring Events — Decoupled Communication Java
// ── 1. Define the event (plain Java record) ───────────────────────────────
public record OrderPlacedEvent(
    String orderId, String customerId, BigDecimal amount, Instant placedAt
) {}

// ── 2. Publish the event ──────────────────────────────────────────────────
@Service
public class OrderService {
    private final ApplicationEventPublisher events; // injected by Spring

    @Transactional
    public Order placeOrder(OrderRequest req) {
        Order order = orderRepository.save(new Order(req));
        // Publish event — synchronous by default (same thread, same transaction)
        events.publishEvent(new OrderPlacedEvent(
            order.getId(), order.getCustomerId(), order.getTotal(), Instant.now()
        ));
        // Synchronous listeners run HERE, within this transaction.
        // If a listener throws, the ENTIRE transaction rolls back.
        return order;
    }
}

// ── 3. Listen to events — unlimited listeners without modifying publisher ─
@Component
public class EmailListener {
    @EventListener
    public void onOrderPlaced(OrderPlacedEvent event) {
        emailService.sendConfirmation(event.customerId(), event.orderId());
    }
}

@Component
public class InventoryListener {
    @EventListener
    public void onOrderPlaced(OrderPlacedEvent event) {
        inventoryService.reserveStock(event.orderId());
    }
}

// ── @Async — run listener in a background thread ─────────────────────────
@Component
public class AnalyticsListener {
    @Async // Runs in a separate thread pool — doesn't block the main request
    @EventListener
    public void onOrderPlaced(OrderPlacedEvent event) {
        analyticsService.trackPurchase(event.amount(), event.customerId());
        // Runs asynchronously — the HTTP response returns before this completes
    }
}

// ── @TransactionalEventListener — fire ONLY after commit ─────────────────
// PROBLEM with @EventListener: if the listener throws, the transaction rolls back.
// If email is sent inside the transaction, you might send an email for an order
// that never got persisted! (Race condition between email and transaction)
@Component
public class SafeEmailListener {
    // PHASE.AFTER_COMMIT: email sent ONLY if the order was actually saved to DB
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onOrderPlaced(OrderPlacedEvent event) {
        emailService.sendConfirmation(event.customerId(), event.orderId());
        // At this point, the DB transaction is committed — safe to email!
    }
}

// ── Built-in Spring lifecycle events ──────────────────────────────────────
@Component
public class ApplicationLifecycleListener {
    @EventListener
    public void onReady(ApplicationReadyEvent event) {
        // ALL beans are fully initialized. Safe to warm caches, start schedulers.
        cacheWarmup.warmAll();
    }

    @EventListener(ContextRefreshedEvent.class)
    public void onContextRefresh() {
        // Fired on startup AND on context refresh (e.g., dev tools reload)
    }

    @EventListener
    public void onShutdown(ContextClosedEvent event) {
        // Flush metrics, close connections, signal health check to return 503
        metricsFlush.flush();
    }
}
💡
Events vs Direct Calls — When to Use Each

Use Spring events when: (1) the publisher shouldn't know who handles the action (decoupling), (2) multiple unrelated components need to react, (3) you want to make a concern pluggable without changing existing code. Use direct service calls when: (1) you need the result of the operation, (2) error handling must be handled in the calling service, (3) the relationship is inherently bidirectional.

Test Your Understanding

Knowledge Check — Dependency Injection

Question 1 of 5
Why is constructor injection preferred over field injection in Spring Boot applications?
Constructor injection is faster at runtime because Spring doesn't use reflection.
Constructor injection allows final fields (immutable), makes dependencies explicit and visible, enables testing without Spring, and fails fast at startup if a dependency is missing.
Field injection was deprecated in Spring 5 — constructor injection is now the only supported method.
Constructor injection uses less memory because the bean is created once and cached.
✅ Constructor injection enforces immutability (final fields), makes all dependencies visible at the class API boundary, allows instantiation without a Spring context (pure Java tests), and provides fail-fast startup behavior. Field injection via @Autowired hides dependencies and requires Spring to inject them via reflection after construction — making the class untestable in isolation.

Knowledge Check — AOP Self-Invocation

Question 2 of 5
A @Service bean has methodA() which calls this.methodB() internally. methodB() is annotated @Transactional(propagation = REQUIRES_NEW). Will a new transaction be created for methodB()?
Yes — REQUIRES_NEW always creates a new transaction regardless of how the method is called.
No — this.methodB() bypasses the Spring proxy, so @Transactional is completely ignored. methodB() runs inside methodA()'s transaction.
Yes — Spring uses bytecode instrumentation to intercept all method calls, including internal ones.
It depends on whether the class implements an interface (JDK proxy) or not (CGLIB).
✅ Spring AOP works via proxy objects. When you call this.methodB(), you're calling on the actual bean object, not the proxy. The proxy never intercepts the call, so no transaction management runs. Fix: extract methodB to a separate bean, or inject self using @Lazy @Autowired private MyService self and call self.methodB().

Knowledge Check — Bean Lifecycle

Question 3 of 5
A bean using field injection tries to call this.repository.count() inside its constructor. What happens?
It works correctly — Spring injects all dependencies before the constructor body runs.
Spring detects this at startup and delays field injection to happen before the constructor body.
NullPointerException at startup — the constructor runs during Phase 1 (instantiation), but @Autowired field injection happens in Phase 2, after the constructor completes.
Spring throws a BeanCreationException before calling the constructor to prevent this scenario.
✅ Field injection happens AFTER the constructor. The field is null during constructor execution. Use @PostConstruct for init logic that needs injected dependencies — it runs in Phase 6, after all deps are set. Better yet: use constructor injection so all dependencies are available the moment the constructor runs.

Knowledge Check — Circular Dependencies

Question 4 of 5
ServiceA requires ServiceB via constructor injection, and ServiceB requires ServiceA via constructor injection. What happens at startup?
Spring resolves it automatically by creating both beans in parallel.
Spring resolves it by creating a proxy for one of the beans during construction.
Spring throws a BeanCurrentlyInCreationException at startup — constructor circular dependencies cannot be resolved and are rejected.
It works if both classes extend a common base class.
✅ Constructor circular dependencies fail at startup with BeanCurrentlyInCreationException. This is intentional — a fail-fast signal that your design has a circular dependency issue. The proper fix is to extract the shared dependency into a third service, or use @Lazy on one constructor parameter if the circular dependency is genuinely unavoidable.

Knowledge Check — Bean Scopes

Question 5 of 5
A singleton @Service is injected with a @Scope("prototype") bean via @Autowired field. How many instances of the prototype bean will be created over the lifetime of the application?
Exactly one — the singleton holds a reference to the one prototype instance injected at startup. The prototype is effectively degraded to singleton behavior.
A new instance per method call on the singleton — Spring refreshes prototype references automatically.
A new instance per HTTP request — Spring detects the request context and refreshes accordingly.
Spring throws an exception — prototype beans cannot be injected into singletons.
✅ When a prototype bean is injected into a singleton via @Autowired, Spring creates one instance at startup and the singleton holds it forever. The prototype scope is effectively ignored. To get a new prototype per use, inject ObjectFactory<PrototypeBean> and call .getObject() each time you need a fresh instance.

Coding Exercises

Exercise 1 — Build a Mini IoC Container
Implement a simplified version of Spring's DI container from scratch using Reflection
Hard

Implement a MiniContainer class that:

1. Accepts class registrations via register(Class<?>)
2. Scans for a custom @Inject annotation on constructor parameters
3. Resolves constructor dependencies recursively
4. Returns fully wired instances via getBean(Class<T>)
5. Caches singleton instances (only one instance per class)

Exercise 2 — Implement a Timing AOP Aspect
Build a production-grade performance logging aspect with thresholds and metrics
Medium

Create a @PerformanceMonitor annotation and an AOP aspect that:

1. Times every method in the service layer
2. Logs a WARNING if execution exceeds a configurable threshold (default: 200ms)
3. Increments a counter metric with method name and outcome (success/error) tags
4. Records execution time as a histogram/timer metric
5. Includes the method name and parameters in the log (sanitize sensitive params)

Production Pitfalls Checklist

🚨
Common Spring Core Mistakes in Production

1. Field injection in new code — Always use constructor injection. Ban @Autowired on fields in your code review checklist.

2. Mutable state in singleton beans — Instance variables in singletons are shared across all threads. Use only stateless singletons, or protect state with locks / atomic types.

3. @Transactional on private methods — Spring AOP cannot proxy private methods. Transaction annotation is silently ignored. Always put @Transactional on public methods.

4. @Transactional on self-invoked methods — Covered above. The proxy is bypassed. Extract to a separate bean.

5. @PostConstruct throwing unchecked exceptions — An exception in @PostConstruct causes the entire application context to fail to start. Always wrap risky logic in try-catch and log rather than propagate (or fail fast intentionally).

6. Not using @PreDestroy for cleanup — Thread pools, DB connections, file handles, and Kafka producers need explicit shutdown. Use @PreDestroy or implement DisposableBean.

7. @Configuration without proxyBeanMethods — If you call a @Bean method from another @Bean method and want singleton behavior, use @Configuration (not @Component) — the CGLIB proxy is what makes it return the same instance.

Section Progress
Mark complete when you understand DI, the bean lifecycle, circular dependencies, and AOP proxies