JavaSpring Boot

Spring Boot Foundations: Dependency Injection and Beans

TT
TopicTrick Team
Spring Boot Foundations: Dependency Injection and Beans

Spring Boot Foundations: Dependency Injection and Beans

"Spring is not just a framework; it is a 'Kernel' for Java Enterprise. It provides the glue that transforms a collection of simple classes into a cohesive, production-ready system."

In the early decades of Java, building complex enterprise systems was a nightmare of manual object wiring. If Class A needed Class B, it manually instantiated it with new B(). This tight coupling made unit testing nearly impossible, forced developers to manage complex lifecycles manually, and led to "Spaghetti Code" where every class was responsible for its own dependencies.

Spring revolutionized this by introducing Inversion of Control (IoC) and Dependency Injection (DI). In this 1,500+ word deep-dive, we go under the hood of the ApplicationContext, analyze the bytecode generation of Proxies, and explore the memory forensics of a production-grade Spring container.


1. The ApplicationContext: The Brain of the System

The ApplicationContext is the container where all your Spring Beans live. It is not a simple "List" or "Map" of objects; it is a sophisticated engine that manages the entire lifecycle of your application, from the first line of code to the final shutdown hook.

The Startup Protocol (The 4 Phases)

When you run SpringApplication.run(YourApp.class, args), the container undergoes a critical, multi-stage startup sequence:

  1. The Scanning & Discovery Phase: Spring searches your classpath for classes annotated with @Component, @Service, or @Repository. It doesn't just "Look" at them; it uses the ASM Bytecode Library to read the class metadata without actually loading the classes into the JVM yet.
  2. The BeanDefinition Stage: For every class found, Spring creates a BeanDefinition. This is a blueprint that describes the bean: its scope (Singleton/Prototype), its dependencies, and whether it should be "Lazy-Loaded" (only created when someone asks for it).
  3. The Post-Processing Stage: Before any objects are created, Spring calls all BeanFactoryPostProcessors. This is how Spring handles property placeholders. For example, if you have ${db.password}, this processor swaps the placeholder for the actual secret value from your environment variables.
  4. The Instantiation & Initialization Stage: This is where the magic happens. Spring creates the actual Java objects, injects their dependencies, and runs any @PostConstruct initialization logic.

2. Dependency Injection: The Internal Mechanics of Wiring

Dependency Injection (DI) is the act of providing a class with its requirements. There are three primary ways to do this, but in 2026, there is only one gold standard.

Constructor Injection: Why it Wins

Constructor injection is the only method that allows your fields to be final. This is critical for Immutable Architecture. When you use constructor injection:

  • Null Safety: You cannot instantiate the class without its dependencies.
  • Circular Dependency Detection: Spring can catch circular dependencies (Class A -> Class B -> Class A) during the instantiation phase rather than failing later during a random runtime method call.
  • Unit Testing: You don't need Spring to test your class. You can just call new UserService(new MockRepository()) in a standard JUnit test.

The Circular Dependency Puzzle (The 3-Level Cache)

What happens if Class A needs Class B, but Class B needs Class A? In standard Java code, this would cause a StackOverflowError. Spring solves this using its "Object Factory" Cache. It creates a "Partially constructed" bean, puts it into a specialized cache, and allows other beans to reference it before it is fully initialized. While Spring handles this for you, circular dependencies are usually a sign of a "God Class" or a "Circular Domain"—if you find yourself relying on this magic, you should likely refactor your code.


3. Bytecode Magic: CGLIB vs. JDK Dynamic Proxies

When you use annotations like @Transactional, @Async, or @Cacheable, Spring doesn't run your code directly. It wraps your class in a Proxy. This is the heart of Aspect-Oriented Programming (AOP).

JDK Dynamic Proxies

If your bean implements an interface (e.g., UserServiceImpl implements UserService), Spring uses the standard JDK Reflection API to create a proxy. This is lightweight but limited to the interface methods.

CGLIB (Code Generation Library)

If your class does not implement an interface, Spring uses CGLIB to generate a new class at runtime that "Extends" your class.

  • The Performance Cost: CGLIB is slightly more memory-intensive because it generates completely new bytecode.
  • The Catch: This is why you cannot use @Transactional on a final class or a final method—CGLIB cannot override them!

4. Bean Scopes and Advanced Lifecycle Hooks

Every Bean has a defined scope that dictates its existence in memory.

  • Singleton (Default): ONE instance per container. This is thread-safe only if your bean is "Stateless" (no instance variables that change).
  • Prototype: A NEW instance every time it is requested. Useful for stateful objects like a user's shopping session.
  • Web Scopes: Request and Session scopes allow you to tie a bean's life directly to an HTTP request, ensuring data is cleared as soon as the user logs off.

Customizing with BeanPostProcessors

If you want to inject custom logic into every bean created by Spring (e.g., automatically logging the execution time of every method), you implement the BeanPostProcessor interface. This is how Spring's internal modules (like Security and Metrics) work—they "Listen" for new beans being born and wrap them in proxies automatically.


5. JVM Memory Forensics: The Cost of Beans

In a large enterprise system with $2,000+$ beans, the ApplicationContext can consume significant memory (often $200$MB - $500$MB just for the container overhead).

  • BeanMetadata Footprint: Spring keeps the "Blueprints" in the heap to allow for dynamic reloading and inspection.
  • Proxy Overhead: Every proxy adds a few kilobytes to your heap. Multiply this by thousands of service calls, and your Metaspace (where class metadata lives) can fill up.

Performance Tip: If you are building a serverless function (like AWS Lambda), use Spring Native with GraalVM. It compiles your beans into a native binary at build-time, removing the "Scanning" phase and dropping your memory usage from $400$MB to $40$MB.


Summary: Designing for Modular Mobility

To master Spring Foundations, you must stop thinking about "Writing code" and start thinking about "Managing an Ecosystem."

  1. Always use Constructor Injection: It ensures your architecture is immutable and testable.
  2. Respect the Proxy: Never call an @Transactional method from within the same class (using this.method()). You will bypass the proxy, and your transaction will simply not exist.
  3. Minimize the Container: If a class doesn't need DI, don't make it a Bean. Keep your container lean to ensure fast startup and low memory usage.

You are no longer just a "Java Developer"; you are a "Systems Architect" who understands the fabric that holds enterprise applications together.


Part of the Java Enterprise Mastery — engineering the foundation.