Error Handling Best Practices in Spring Boot with Kotlin
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
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.
To follow this tutorial, you will need:
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:
@Service
annotation marks this class as a service componentSpring beans are objects that are managed by Spring's IoC (Inversion of Control) container. You can create beans in two ways:
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:
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()
}
}
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 injectedConstructor 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)
}
}
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)
}
}
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
}
}
If you see a "No qualifying bean" error, check:
@SpringBootApplication // This enables component scanning
class MyApplication
fun main(args: Array<String>) {
runApplication<MyApplication>(*args)
}
@Service // Make sure you have the correct annotation
class UserService(private val repository: UserRepository)
You've learned how to:
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
Learn how to secure your Spring Boot REST APIs using Kotlin with industry-standard security practices and implementations.
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.