Skip to content
Blog

The hidden lessons of Spring: one framework, three perspectives

Pieter Vercammen
Pieter Vercammen
Written on 21 Aug 2026
5 min read
Back-End Dependency Injection Java Spring AOP Spring Framework

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.

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.

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.

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.

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.

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.

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.

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.

The Debugging Detective: JProfiler vs. IntelliJ Profiler 
Blog </> · 3 min read

The Debugging Detective: JProfiler vs. IntelliJ Profiler 

Blog </> · 3 min read

TDD for readable code

Blog </> · 2 min read

End-to-end tracing: gaining control over the chaos of microservices!

Blog </> · 4 min read

Mowing repetitive code with an often forgotten annotation

Let's get to work!

Ready to turn your vision into reality?
Let's build the systems that power your growth.