How To Use Dependency Injection and Beans in Spring Boot with Kotlin

December 30, 2024 3 min read Intermediate

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:

  1. Detect these classes through component scanning
  2. Create beans for them
  3. 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 requests
  • MessageGenerator 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:

  1. Component scanning is enabled:
@SpringBootApplication  // This enables component scanning
class MyApplication

fun main(args: Array<String>) {
    runApplication<MyApplication>(*args)
}
  1. 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

Latest Articles

Error Handling Best Practices in Spring Boot with Kotlin

Error Handling Best Practices in Spring Boot with Kotlin

4 min read rest · Spring Basics

Master Spring Boot error handling in Kotlin with comprehensive examples: learn to implement global exception handlers, custom error responses, and production-ready error handling strategies

Securing Spring Boot REST APIs with Kotlin: Best Practices Guide

Securing Spring Boot REST APIs with Kotlin: Best Practices Guide

4 min read Security · Spring Basics

Learn how to secure your Spring Boot REST APIs using Kotlin with industry-standard security practices and implementations.

Request Validation in Spring Boot with Kotlin: A Complete Guide

Request Validation in Spring Boot with Kotlin: A Complete Guide

4 min read rest · Spring Basics

Learn how to implement robust request validation in Spring Boot with Kotlin, from basic field validation to custom validators, complete with practical examples and best practices.