Article

How to inject multiple implementations in the Spring Framework

An exploration of the different strategies Spring Framework offers for injecting multiple implementations of the same interface into a single bean.

Editor’s Note: This post was updated to add a section on controlling the order in which Spring injects beans into a List, using the @Order annotation and the Ordered interface.

Recently, I had a discussion with one of my colleagues during a code review. We talked about a hidden gem in Spring (as well as other frameworks like Micronaut) that could simplify our code.

Let’s consider a task: developing a good morning greeting system that supports both English and Hebrew languages. The system should greet us in each of these languages.

We’ll begin with the native implementation by introducing our greeters:

class HebrewGreeter {
    fun sayGoodMorning() {
        println("בוקר טוב")
    }
}

class EnglishGreeter {
    fun sayGoodMorning() {
        println("Good morning")
    }
}

Next, we’ll create a class to hold the greeters and invoke them:

@Service
class GreeterService(
    private val hebrewGreeter: HebrewGreeter = HebrewGreeter(),
    private val englishGreeter: EnglishGreeter = EnglishGreeter()
) {
    fun greetInAllLanguages() {
        hebrewGreeter.sayGoodMorning()
        englishGreeter.sayGoodMorning()
    }
}

This implementation works as expected. However, imagine that we want to introduce a new greeter, such as a German greeter. In that case, we would need to modify the GreeterService class, which violates the open-closed principle from the SOLID principles.

To address this issue, we’ll introduce an interface that encapsulates the common functionality of all greeters:

interface Greeter {
    fun sayGoodMorning()
}

@Component
class HebrewGreeter : Greeter {
    override fun sayGoodMorning() {
        println("בוקר טוב")
    }
}

@Component
class EnglishGreeter : Greeter {
    override fun sayGoodMorning() {
        println("Good morning")
    }
}

@Component
class GermanGreeter : Greeter {
    override fun sayGoodMorning() {
        println("Guten Morgen")
    }
}

Now comes the exciting part. Since we’ve implemented an interface, the GreeterService can request Spring to inject all available implementations into its constructor:

@Service
class GreeterService(private val greeters: List<Greeter>) {
    fun greetInAllLanguages() {
        greeters.forEach { it.sayGoodMorning() }
    }
}

Our code is now much simpler, but that’s not the only benefit. If we introduce a new greeter in the future, it will be automatically added to the system without requiring any modifications to the code!

Another option instead of using List<Greeter> would be to use a Map<String, Greeter>. In this case, Spring would inject a map where the keys are the full class names and the values are the corresponding beans. For example, com.example.greeter.EnglishGreeter.

As a side note, the order in which Spring injects the beans into the List is not guaranteed by default. When the order matters, you can control it in two ways. In both, a lower value means higher precedence, so the bean appears earlier in the list.

The @Order annotation gives you static ordering, with the position fixed by the annotation value:

@Component
@Order(1)
class EnglishGreeter : Greeter {
    override fun sayGoodMorning() {
        println("Good morning")
    }
}

@Component
@Order(2)
class GermanGreeter : Greeter {
    override fun sayGoodMorning() {
        println("Guten Morgen")
    }
}

The Ordered interface gives you dynamic ordering, where getOrder() can derive the position from the bean’s own configuration or state rather than a hardcoded annotation value:

@Component
class HebrewGreeter(
    @Value("\${greeter.hebrew.order:3}") private val order: Int
) : Greeter, Ordered {
    override fun sayGoodMorning() {
        println("בוקר טוב")
    }

    override fun getOrder(): Int = order
}
Share