Category: Spring Framework


  • The Proxy Paradox: Why Spring @Transactional Vanishes

    We’ve all been there. You annotate @Transactional on a critical method in your Spring application, run mvn test, watch the green checkmarks fly by, and feel good about yourself. Everything’s going swell innit?

    But then you open the transaction log and find… nothing. Where did your transaction go?

    No connection enlisted. No timeout. No rollback on error. The code did execute, but a transaction was not created.

    Congratulations, you’ve just met the Proxy Paradox. The coding equivalent to plugging in your phone overnight and waking up to 7% battery.

    Stick around for a few minutes, and you’ll know why this happens, and how to mitigate this behavior with some well-known patterns.

    Understanding Spring AOP

    The seeds of this bug are planted in Spring’s Aspect Oriented Programming (AOP).

    In Spring, AOP is used to decouple cross-cutting concerns (like logging, security, or transactions) from your core business logic to keep code modular. It achieves this by wrapping your beans in dynamic proxies that intercept method calls to inject this extra behavior at runtime — without modifying the original code.

    Spring AOP is how @Transactional annotation works in the first place.

    When a Spring container starts up, it scans your beans. It asks, “Hey, does this class have any aspect-related annotations, like @Transactional, @Async, or @Cacheable?”

    If the answer is yes, it doesn’t give you the raw bean. It wraps that bean in a proxy (either a JDK dynamic proxy or a CGLIB-generated subclass). This wrapper intercepts calls from the outside world and funnels them through an interceptor chain.

    However, the interceptor chain does not come into picture if a call comes internally, i.e., from within the class.

    The Bug in action

    Take a look at the following code snippet:

    @Service
    class WalletService {
    
        // The entry point
        public void pay(BigDecimal amount) {
            // The internal call causing the issue
            withdrawMoney(amount); 
        }
    
        @Transactional
        public void withdrawMoney(BigDecimal amount) {
            // ... complex logic with database writes ...
        }
    }
    

    Here, withdrawMoney() is marked @Transactional. Any calls to this method from outside of WalletService (say, a controller) work as expected. The call goes through the proxy, a transaction is started, and then the raw bean’s method is executed.

    However, if the call to withdrawMoney() comes through pay(), it executes non-transactionally.

    Why? Because the call to withdrawMoney() happens inside the raw bean, bypassing the proxy completely. Spring’s TransactionInterceptor never comes into picture. No connection is bound to the thread. No commit. No rollback.

    But… sometimes it works!

    If you have a colleague who claims that this works, they’re probably using AspectJ Load-Time Weaving:

    @EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
    

    AspectJ is different. It doesn’t use proxies; it modifies the actual bytecode of your class during class loading. It literally weaves the transaction logic into your original method.

    • Pros: Self-invocation works perfectly.
    • Cons: Requires a special Java agent, adds complexity to the build process, increases startup time, and is generally overkill for standard web apps.

    Practical Fixes

    So, you’re stuck with the proxy issue. How do you fix it? Here are the top 5 solutions, ranked from “Best Practice” to “Please Don’t Do This”:

    1. Refactor (Recommended)

    Move withdrawMoney() to its own @Service:

    @Service
    class PaymentService {
        private final WalletService walletService; // Inject dependency
        
        public void pay(BigDecimal amount) {
            walletService.withdrawMoney(amount); // External call!
        }
    }
    

    This is the cleanest solution. It fits SOLID principles, and makes unit testing much easier.

    2. Self-Injection

    You can actually ask Spring to inject the proxy into the bean itself:

    @Service
    class WalletService {
        @Lazy 
        @Autowired 
        private WalletService self;
    
        public void pay(BigDecimal amount) {
            self.withdrawMoney(amount); // Goes through the proxy
        }
    }
    

    This works, but feels weird. It also makes use of Field Injection, which is not considered a best practice.

    This approach will not work at all if you use Constructor Injection (you will be hit with circular reference errors).

    3. Programmatic Transactions

    Why not introduce an explicit transaction?

    transactionTemplate.execute(status -> {
        withdrawMoney(amount);
        return null;
    });
    

    This is the easiest and simplest fix. It does add some boilerplate code, but clarity beats magic any day.

    4. AopContext.currentProxy()

    You can force the method call through the AOP Proxy:

    ((WalletService) AopContext.currentProxy()).withdrawMoney(amount);
    

    This will route the internal method call through the proxy. This works, but it makes the AOP abstraction leaky. Your business logic is forced to learn framework details. Purists will frown at this, but it will also come in your way if you want to migrate to AspectJ later. Use sparingly.

    5. AspectJ Load-Time Weaving

    We’ve seen this earlier:

    @EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
    

    This is great if you want self-invocation across thousands of beans. But it introduces a lot of complexity, and is rarely worth it for a handful of cases.

    Summary

    The Proxy Paradox is a rite of passage for Spring developers. Just remember the flow:

    1. External Call → Proxy → Aspects run → Logic runs.
    2. Internal Call → Raw this object → Logic runs (No Aspects).

    Re-organize your methods, self-inject if you must, or drop down to TransactionTemplate—but never trust @Transactional on a self-invoked method again.


    Sound Off: What quirky Spring “gotcha” cost you the most debug minutes? Drop a comment below with the annotation that betrayed you!


  • The Dependency Injection Dilemma: Why I’m Finally Ghosting @Autowired on Fields

    In the world of Spring Boot development, we are often seduced by “magic.”

    We love the annotations that make 50 lines of boilerplate vanish. We love the auto-configuration that “just works.” And for a long time, the poster child for this magic was the @Autowired annotation sitting snugly atop a private field. It looks clean, it’s remarkably easy to write, and it feels like the pinnacle of modern Java productivity.

    But as I’ve spent more time in the trenches of large-scale enterprise architecture, I’ve realized that field injection is a siren song. It promises a shortcut but leads you straight into a rocky shore of un-testable code, hidden dependencies, and runtime nightmares.

    Today, I’m making the case for the “Old Reliable” of the Java world: Constructor Injection. It’s time to stop using field injection, and it’s not just because the Spring documentation tells you to. It’s because your architecture deserves better.

    The Aesthetic Trap: Why we fell in love with Field Injection

    Before we tear it down, we have to acknowledge why we used it in the first place. Take a look at the following code snippet:

    @Service
    public class OrderService {
    
        @Autowired
        private UserRepository userRepository;
    
        @Autowired
        private PaymentService paymentService;
    
        @Autowired
        private InventoryClient inventoryClient;
    
        // Business Logic...
    
    }
    

    It’s undeniably sleek. There are no bulky constructors taking up half the screen. It feels like the “Spring Way.” For years, this was the standard in tutorials and stack overflow answers. It allowed us to add a dependency with a single line of code.

    However, this “cleanliness” is a visual illusion. It’s like hiding a messy room by shoving everything into a closet. The room looks clean, but you’ve actually made the system harder to manage.

    The Case for Immutability

    As engineers, we should strive for Immutability. An object that cannot change after it is created is inherently safer, more predictable, and easier to reason about in a multi-threaded environment.

    When you use field injection, you cannot declare your dependencies as final. Spring needs to be able to reach into your object after the constructor has run to inject those fields via reflection. This means your dependencies are technically mutable.

    By switching to Constructor Injection, you regain the ability to use the final keyword:

    @Service
    public class OrderService {
        private final UserRepository userRepository;
        private final PaymentService paymentService;
        private final InventoryClient inventoryClient;
    
        public OrderService(
            UserRepository userRepository, 
            PaymentService paymentService,
            InventoryClient inventoryClient) {
            this.userRepository = userRepository;
            this.paymentService = paymentService;
            this.inventoryClient = inventoryClient;
        }
    }
    

    Now, your class is “Born Ready.” Once the OrderService exists, you have a 100% guarantee that the userRepository is there and will never be changed or set to null by some rogue process. This is the foundation of thread safety and defensive programming.

    The Case for Unit Testing

    If you want to know how good your architecture is, look at your unit tests. If your test setup looks like a ritualistic sacrifice, your architecture is broken.

    Field injection makes unit testing unnecessarily difficult. Because the fields are private and Spring is doing the heavy lifting behind the scenes, you can’t simply instantiate the class in a test. You have two bad options:

    1. Use Spring in your tests: You use @SpringBootTest or @MockBean. Now your “unit” test is starting a miniaturized version of the Spring Context. It’s slow, it’s heavy, and it’s no longer a unit test! (Hint: It’s an integration test!)
    2. Use Reflection: You use ReflectionTestUtils to manually “shove” a mock into a private field. This is brittle. If you rename the field, your test breaks, but your compiler won’t tell you why.

    With Constructor Injection, testing is a breeze. Since the constructor is the only way to create the object, you just pass the mocks in directly:

    @Test
    void shouldProcessOrder() {
        UserRepository mockUserRepo = mock(UserRepository.class);
        PaymentService mockPaymentService = mock(PaymentService.class);
        InventoryClient mockInventoryClient = mock(InventoryClient.class);
    
        // Standard Java. No magic. No Spring. Fast.
        OrderService service = new OrderService(mockUserRepo, mockPaymentService, mockInventoryClient);
    
        service.process(new Order());
    }
    

    Failing Fast: The 2:00 AM Production Bug

    We’ve all been there. You deploy a change, the app starts up fine, and everything looks green. Then, at 2:00 AM, a specific user hits an edge-case API endpoint, and the logs explode with a NullPointerException.

    Why? Because with field injection, Spring allows the application to start even if a dependency is missing or circular. The field just remains null. You don’t find out until the code actually tries to use that field.

    Constructor Injection is your early warning system. Because Spring must call the constructor to create the bean, it must satisfy all dependencies immediately. If a bean is missing, the ApplicationContext will fail to load. The app won’t even start on your machine, let alone in production.

    I’d much rather spend 5 minutes fixing a startup error on my local machine than 5 hours explaining to a stakeholder why the payment service crashed in the middle of the night.

    The Single Responsibility Principle

    The Single Responsibility Principle (SRP) states that a class should have one, and only one, reason to change.

    Field injection makes it too easy to violate this. Because each dependency is just one line of code, you don’t notice when a class starts doing too much. I’ve seen services with 15 @Autowired fields that looked “neat” on the screen.

    When you use Constructor Injection, a class with 15 dependencies looks like a monster. The constructor is massive. It’s hard to read. It’s ugly.

    And that is exactly the point. That “Constructor of Doom” is a signal. It’s the code telling you: “Hey, I’m doing too much. Please refactor me into smaller, more focused services.” Field injection is like a layer of makeup that hides a skin infection; Constructor Injection forces you to see the problem and treat it.

    Circular Dependencies: The Infinite Loop

    Circular dependencies (Service A needs B, and B needs A) are usually a sign of poor design. However, field injection allows them to happen almost unnoticed. Spring will try to resolve them using proxies, often leading to confusing behavior.

    Constructor Injection doesn’t allow circular dependencies by default. If you try it, Spring will throw a BeanCurrentlyInCreationException.

    While this might seem like a nuisance, it’s actually a guardrail. It forces you to rethink your service boundaries. Usually, a circular dependency means you need a third service (Service C) to hold the shared logic, or you need to move to an event-driven approach.

    The Lombok Cheat Code

    The most common pushback I hear is: “But I don’t want to write and maintain constructors for 200 services!”

    I agree. I’m a programmer; if I can automate a task, I will. This is where Project Lombok becomes your best friend.

    By using the @RequiredArgsConstructor annotation, you get the best of both worlds. You declare your fields as private final, and Lombok generates the constructor at compile time.

    @Service
    @RequiredArgsConstructor
    public class OrderService {
        private final UserRepository userRepository;
        private final PaymentService paymentService;
        private final InventoryClient inventoryClient;
    
        // No manual constructor needed!
    }
    

    Professionalism is in the Details

    At the end of the day, using Constructor Injection is about intentionality. It’s about making a conscious choice to write code that is framework-independent, easy to test, and architecturally sound. It’s about moving away from “Spring Magic” and moving toward “Java Excellence.”

    If you’re working on a legacy codebase filled with @Autowired fields, don’t panic. You don’t have to refactor everything tonight. But for every new service you write, try the constructor approach. Notice how your tests become simpler. Notice how your classes become smaller.

    Your code is a reflection of your craftsmanship. Don’t let a shortcut like field injection be the thing that undermines it.


    What’s your take?

    Are you a die-hard @Autowired fan, or have you embraced the constructor? Let’s talk about it in the comments. If you found this helpful, consider sharing it with a junior dev who is still caught in the “Field Injection Trap.”