
How To Use Dependency Injection and Beans in Spring Boot with Kotlin
Learn how to implement dependency injection and manage Spring beans in Kotlin Spring Boot applications for cleaner, more maintainable code with practical examples and best practices.
Introduction
Dependency injection (DI) and beans are fundamental concepts in Spring Boot that help you write maintainable and testable applications. In this tutorial, you'll learn how to use dependency injection and manage Spring beans effectively in your Kotlin Spring Boot applications.
Prerequisites
To follow this tutorial, you will need:
- Kotlin 1.9.0 or higher installed
- Java Development Kit (JDK) 17 or higher installed
- An Integrated Development Environment (IDE) like IntelliJ IDEA
- Basic knowledge of Kotlin and Spring Boot
- Gradle or Maven build tool configured
Step 1 — Understanding Dependency Injection
Dependency injection is a design pattern where a class receives its dependencies from external sources rather than creating them. Let's look at an example without dependency injection first:
// Without dependency injection
class UserService {
// UserService creates its own dependency
private val userRepository = UserRepository()
fun getUser(id: Long): User {
return userRepository.findById(id)
}
}
In this example, UserService
creates its own UserRepository
instance, making the code tightly coupled and difficult to test.
Now, let's improve this code using dependency injection:
// With dependency injection
@Service
class UserService(
private val userRepository: UserRepository
) {
fun getUser(id: Long): User {
return userRepository.findById(id)
}
}
You'll notice that:
- The dependency is now passed through the constructor
- Spring's
@Service
annotation marks this class as a service component - The code is more flexible and easier to test
Step 2 — Creating and Managing Spring Beans
Spring beans are objects that are managed by Spring's IoC (Inversion of Control) container. You can create beans in two ways:
Method 1: Using Component Scanning
Create a new file called UserRepository.kt
:
@Repository
class UserRepository {
fun findById(id: Long): User {
// Database access logic here
return User(id, "John Doe")
}
}
Then create UserService.kt
:
@Service
class UserService(
private val repository: UserRepository
) {
fun getUser(id: Long): User {
return repository.findById(id)
}
}
Spring will automatically:
- Detect these classes through component scanning
- Create beans for them
- Wire the dependencies together
Method 2: Manual Bean Declaration
Create a configuration class called AppConfig.kt
:
@Configuration
class AppConfig {
@Bean
fun userService(userRepository: UserRepository): UserService {
return UserService(userRepository)
}
@Bean
fun userRepository(): UserRepository {
return UserRepository()
}
}
Step 3 — Understanding Bean Scopes
Spring beans can have different scopes that determine their lifecycle. Let's explore the most common ones:
// Singleton scope (default)
@Service
class UserService(private val repository: UserRepository)
// Prototype scope - new instance for each injection
@Service
@Scope("prototype")
class MessageGenerator {
fun generateMessage(): String {
return "Message ${Random.nextInt()}"
}
}
To test the different scopes, create a simple controller:
@RestController
class TestController(
private val userService: UserService,
private val messageGenerator: MessageGenerator
) {
@GetMapping("/test")
fun test(): String {
val message = messageGenerator.generateMessage()
return "Message: $message"
}
}
When you run this code and make multiple requests to /test
, you'll notice:
UserService
will be the same instance for all requestsMessageGenerator
will be a new instance each time it's injected
Step 4 — Implementing Constructor Injection
Constructor injection is the recommended way to inject dependencies in Spring Boot. Here's how to implement it:
@Service
class EmailService {
fun sendEmail(to: String, message: String) {
println("Sending email to $to: $message")
}
}
@Service
class UserNotificationService(
private val userService: UserService,
private val emailService: EmailService
) {
fun notifyUser(userId: Long, message: String) {
val user = userService.getUser(userId)
emailService.sendEmail(user.email, message)
}
}
Step 5 — Testing with Dependency Injection
Create a test file called UserServiceTest.kt
:
@SpringBootTest
class UserServiceTest {
@Autowired
lateinit var userService: UserService
@MockBean
lateinit var userRepository: UserRepository
@Test
fun `should return user when found`() {
// Given
val userId = 1L
val expectedUser = User(userId, "John Doe")
given(userRepository.findById(userId)).willReturn(expectedUser)
// When
val result = userService.getUser(userId)
// Then
assertThat(result).isEqualTo(expectedUser)
}
}
Step 6 — Handling Common Issues
Issue 1: Circular Dependencies
If you encounter circular dependencies, here's how to resolve them:
// Problem:
@Service
class ServiceA(private val serviceB: ServiceB)
@Service
class ServiceB(private val serviceA: ServiceA)
// Solution:
@Service
class ServiceA {
@Autowired
lateinit var serviceB: ServiceB
fun doSomething() {
serviceB.process()
}
}
@Service
class ServiceB {
@Autowired
lateinit var serviceA: ServiceA
fun process() {
// Implementation
}
}
Issue 2: No Bean Found Exception
If you see a "No qualifying bean" error, check:
- Component scanning is enabled:
@SpringBootApplication // This enables component scanning
class MyApplication
fun main(args: Array<String>) {
runApplication<MyApplication>(*args)
}
- The bean is properly annotated:
@Service // Make sure you have the correct annotation
class UserService(private val repository: UserRepository)
Conclusion
You've learned how to:
- Implement dependency injection in Spring Boot with Kotlin
- Create and manage Spring beans
- Use different bean scopes
- Write testable code using constructor injection
- Handle common dependency injection issues
Next Steps
- Learn how to configure Spring Boot applications using properties and YAML files
- Explore Spring profiles for environment-specific configurations
- Study Spring Boot's testing capabilities in depth