Safely Customize Spring Boot Starters with BeanPostProcessor
Safely Customize Spring Boot Starters with BeanPostProcessor

How to Replace a Spring Boot Starter Bean Without Code Duplication

Learn how to safely customize Spring Boot starter beans using BeanPostProcessor to avoid duplication and cyclic dependencies.7 min


When working with Spring Boot, the framework encourages us to quickly set up applications through powerful starter dependencies. These starters conveniently provide auto-configuration and sensible defaults for beans, allowing us to focus more on application logic rather than tedious boilerplate.

However, as your application matures, you often encounter scenarios where the provided starter bean doesn’t quite meet your application’s exact requirements. You might need a slightly modified version—perhaps an additional logging behavior, altered initialization parameters, or extended configurations. The common approach is to replace these default beans. But copying and pasting code wholesale isn’t just tedious—it introduces maintainability headaches and risks falling out-of-sync with library updates.

Let’s take a closer look at the traditional method most developers initially try, analyze its pitfalls, and then explore a smarter way of replacing Spring Boot starter beans without duplicating code.

The Traditional Approach to Replacing Spring Boot Starter Beans

Spring Boot starters usually define beans using methods annotated with @Bean, often combined with conditional annotations like @ConditionalOnMissingBean. Let’s analyze an example of such bean definition from a typical starter class:

@Bean
@ConditionalOnMissingBean
public MyService defaultMyService() {
    MyService service = new MyService();
    service.setConfig(defaultConfig());
    service.initialize();
    return service;
}

In this snippet, the @Bean annotation instructs Spring to register the method’s return as a bean within the application context. The @ConditionalOnMissingBean annotation tells Spring to only use this definition if no other bean of type MyService already exists.

Importantly, this pattern allows you the flexibility to “override” or “replace” this default bean by simply providing your own bean definition. Suppose you’re not happy with the starter’s configuration—perhaps the defaults aren’t optimized for your production needs—you typically create your own configuration class:

@Configuration
public class MyConfiguration {
    
    @Bean
    public MyService myCustomService() {
        MyService service = new MyService();
        service.setConfig(customConfig());
        service.initialize();
        return service;
    }
}

Notice that the custom bean method above looks almost identical to the original starter bean. This means you’re essentially duplicating code from the starter library just to inject different settings. Duplicating code might seem harmless at first, but it creates unnecessary dependencies on internal implementation details and increases risks. Changes or improvements in future releases of the starter will have to be manually replicated in your custom implementation.

Why Code Duplication is Problematic

There are several issues that come with duplication:

  • Maintenance burden: Every time the starter officially updates or improves the original bean logic, your duplicated bean becomes stale and needs manual updating.
  • Error-prone adjustments: Small mistakes happen easily during the copy-and-paste routine. Such hidden bugs waste valuable development and debugging hours later.
  • Reduced readability & clarity: Duplicated code reduces the clarity of your codebase, making it harder for new developers to recognize core behaviors and customizations.

Clearly, we need a better solution.

An Alternative Approach: Leveraging Dependency Injection

Instead of duplicating code directly, consider leveraging dependency injection to reconfigure or adjust the starter bean. Think of dependency injection like modifying a recipe by slightly changing ingredients rather than rewriting it entirely from scratch.

Let’s try using the starter-defined bean to create our customized bean directly:

@Configuration
public class MyConfiguration {

    @Bean
    public MyService myCustomService(MyService starterService) {
        starterService.setConfig(customConfig()); // attempt to reuse the starter bean directly
        return starterService;
    }
}

However, experimenting with this simple injection may cause Spring Boot to detect a cyclic dependency and raise an error:

org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'myCustomService':
Requested bean is currently in creation: Is there an unresolvable circular reference?

This happens when the bean dependency graph references itself in a cycle. Simply put, Spring cannot inject the bean currently being instantiated.

Understanding and Preventing Cyclic Dependencies

Cyclic dependencies occur because the custom bean method depends on the same bean type currently being created, confusing Spring’s initialization order.

A smart way to handle this nuance is by using a Spring feature known as BeanPostProcessor. BeanPostProcessors let you tap into the lifecycle of bean initialization without changing the fundamental bean creation logic, effectively avoiding cyclic dependencies.

Implementing a BeanPostProcessor to Solve Cyclic Dependencies

By implementing the BeanPostProcessor interface, you can intercept beans after Spring creates them, enabling safe modifications. Here’s how you can do this practically:

@Component
public class MyServiceCustomizer implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        if (bean instanceof MyService) {
            MyService service = (MyService) bean;
            service.setConfig(customConfig()); // customize existing bean here
        }
        return bean;
    }
    
    private MyConfig customConfig() {
        // define your custom configurations here
        MyConfig config = new MyConfig();
        config.setOption("advancedMode", true);
        return config;
    }
}

This code cleanly solves our problem:

  • It removes duplication because we’re reusing, not re-defining, the starter bean’s original logic.
  • It ensures future-proofing since upgrades to the starter won’t require manual synchronization.
  • It elegantly avoids cyclic dependencies and keeps your application context functional.

Benefits of Using BeanPostProcessor for Bean Customization

  • No duplication: Use the original starter-defined logic, making your application DRY (Don’t Repeat Yourself).
  • Cleaner, maintainable code: Reduced complexity and clearer separation of concerns.
  • Future-proof and resilient: Upgrading starters becomes breezy, as customization logic remains isolated and non-invasive.

Apply This Technique To Your Projects

Replacing a default Spring Boot starter bean without duplication is more than just good practice; it’s essential when aiming for maintainable and robust Spring applications. Leveraging BeanPostProcessor effectively achieves this goal.

Next time you hit the familiar issue of customizing a starter bean, resist the temptation of copying boilerplate code. Instead, embrace smarter dependency injection and Bean customization. Your future self—and everyone else that maintains your code—will thank you.

Now, take a few minutes to assess your current Spring Boot projects. Are there spots where unnecessary duplication exists? Could this simple but powerful strategy streamline your application? Give it a try and discover cleaner, more maintainable codebases. If you’re looking to learn more advanced Java and Spring concepts, be sure to check out my comprehensive guides and articles available on my website.

What other techniques do you use to avoid duplication in your Spring projects? I’d love to hear your insights or questions in the comments below!


Like it? Share with your friends!

Shivateja Keerthi
Hey there! I'm Shivateja Keerthi, a full-stack developer who loves diving deep into code, fixing tricky bugs, and figuring out why things break. I mainly work with JavaScript and Python, and I enjoy sharing everything I learn - especially about debugging, troubleshooting errors, and making development smoother. If you've ever struggled with weird bugs or just want to get better at coding, you're in the right place. Through my blog, I share tips, solutions, and insights to help you code smarter and debug faster. Let’s make coding less frustrating and more fun! My LinkedIn Follow Me on X

0 Comments

Your email address will not be published. Required fields are marked *