Blog
The hidden lessons of Spring: one framework, three perspectives
How does your perspective on the Spring framework change as you gain experience? Discover the key lessons and pitfalls for junior, mid-level and senior developers.
The Spring framework has been one of the most widely used frameworks in the Java ecosystem for years. With Dependency Injection and an extensive container model, it removes a great deal of complexity for developers.
But your perspective on Spring changes as your experience grows. The questions you ask as a junior developer are fundamentally different from those you ask as a mid-level or senior developer. Not because the framework changes, but because you gradually discover more of what is happening behind the scenes.
In this blog, we explore how developers at different experience levels look at the Spring framework and the insights they gain at each stage.
1. Junior developer: learning to trust the container
When you start working with Spring, everything revolves around one fundamental principle: the container takes over responsibilities you previously managed yourself. Instead of creating objects manually with the new keyword, you let the Spring container determine when and how dependencies are injected through Inversion of Control (IoC) and Dependency Injection (DI).
Best practice: Constructor vs. Setter Injection
Use Constructor Injection for required dependencies. It enables immutable objects, guarantees that dependencies are never null, and makes your code significantly easier to test.
Use Setter Injection only for optional dependencies that can have a meaningful default value.
The pitfall: circular dependencies
A classic pitfall for junior developers is creating a circular dependency. If Class A requires Class B in its constructor and Class B, in turn, requires Class A, Spring cannot start the application and throws a BeanCurrentlyInCreationException.
The solution
Often, the best approach is not a technical workaround, such as using Setter Injection as a last resort, but a better design that decouples the components.
2. Mid-level developer: understanding unexpected Spring behavior
At this level, the focus shifts. You no longer use the Spring framework blindly; you also want to understand why certain things happen. The emphasis is on mastering Bean Scopes, proxies, and Aspect-Oriented Programming (AOP).
Common pitfall #1: Singleton versus Prototype
Many developers expect a Prototype Bean to always produce a new instance. But what happens when a Singleton Bean, which is created only once, depends on a Prototype Bean that is supposed to provide a new instance each time?
The Prototype Bean is injected only once, when the Singleton Bean is created. After that, you keep getting the same “prototype” instance, which can lead to unexpected behavior.
The solution
Use Method Injection, such as @Lookup, or an ObjectProvider to retrieve a new instance whenever one is needed.
Common pitfall #2: the “lite” mode trap (@Configuration vs. @Component)
A @Bean method inside a class annotated with @Component behaves fundamentally differently from one inside a class annotated with @Configuration. A small difference in annotation can lead to a completely different outcome.
- Full mode (@Configuration): Spring uses CGLIB subclassing to ensure that calling a @Bean method always returns the same cached Singleton instance.
- Lite mode (@Component): Spring treats this as a regular factory method. If you call the method manually, for example otherBean() from another @Bean method, you get a new instance outside the container’s management. As a result, scoping, AOP, and other post-processors are no longer applied correctly to that call.
Common pitfall #3: the AOP self-invocation trap – why does @Transactional sometimes not work?
In standard proxy-based Spring AOP, proxies are used to add functionality such as transaction management.
If a class calls one of its own methods internally, for example this.bar() from foo(), the call does not pass through the proxy. As a result, aspects applied to bar(), such as @Transactional or @Async, are unexpectedly not executed.
3. Senior developer: understanding what happens behind the scenes
For senior developers, the focus shifts once again. It is no longer primarily about using Spring or explaining unexpected behavior, but about understanding the container itself, the bean lifecycle, container extensibility, and the underlying Spring infrastructure.
You are no longer just using the container; you are extending it.
Key concept #1: the “power move” – BFPP vs. BPP
Understanding the difference between a BeanFactoryPostProcessor (BFPP) and a BeanPostProcessor (BPP) is essential when working with infrastructure code. It forms the foundation of many Spring features:
- BFPP (BeanFactoryPostProcessor): works on bean configuration metadata before any beans are actually created. This is where you can modify properties using mechanisms such as placeholders.
- BPP (BeanPostProcessor): works on the actual bean instances after they have been created and configured. This is how Spring handles features such as @Autowired and AOP proxies.
The senior pitfall: early initialization
Be extremely careful when injecting dependencies into a @Configuration class or a BeanPostProcessor. Because these are processed very early in the lifecycle, they can cause other beans to be initialized prematurely. When that happens, those beans may no longer be processed correctly by later BPPs, such as AOP auto-proxying.
The solution
Use static @Bean methods for post-processor definitions such as BPPs and BFPPs to prevent the surrounding configuration class from being instantiated too early.
Key concept #2: advanced lifecycle management
In larger applications, the order in which components start becomes increasingly important. For complex startup requirements, relying on @PostConstruct alone is often not enough. This is where SmartLifecycle becomes relevant.
It allows you to define specific phases using getPhase(), so that, for example, a message listener only starts after database connections and caches are fully ready.
Conclusion
Although every developer works with the same Spring framework, the focus shifts as experience grows. A junior developer learns to trust the container. A mid-level developer tries to explain unexpected behavior. A senior developer focuses on the mechanisms behind the scenes and understands how Spring organizes itself and how that behavior can be extended or influenced.
The Spring framework does not change. The perspective you bring to the framework does.
< Keep reading />
More from our team
Explore more insights, tips, and deep dives from the CraftCode team.
Let's get to work!
Ready to turn your vision into reality?
Let's build the systems that power your growth.