Have you ever run into a StackOverflowError
when starting your application and wondered what went wrong? If you're working with dependency injection in Kotlin, chances are you've encountered the dreaded circular dependency problem. In this post, I'll share how I solved this issue in a real-world application using the Mediator Pattern.
The Problem: When Dependencies Go Round and Round
Picture this scenario: You have two repositories that need each other to function properly.
-
TokenRepositoryImpl
needsUsersRepository
to look up user profiles -
UsersRepositoryImpl
needsTokenRepository
to invalidate tokens
This creates a classic chicken-and-egg problem. When your dependency injection framework (in my case, Koin) tries to create these objects, it gets caught in an infinite loop:
- To create
TokenRepositoryImpl
, it needs to createUsersRepository
first - To create
UsersRepository
, it needs to createTokenRepository
first - Back to step 1, and... 💥 BOOM!
StackOverflowError
Here's what that looks like in the wild:
Exception in thread "main" java.lang.StackOverflowError
at kotlin.reflect.jvm.internal.KClassImpl.hashCode(KClassImpl.kt:306)
at java.base/java.util.concurrent.ConcurrentHashMap.get(ConcurrentHashMap.java:936)
at org.koin.ext.KClassExtKt.getFullName(KClassExt.kt:25)
...many more lines of recursive calls...
The Solution: Enter the Mediator Pattern
The mediator pattern is a behavioral design pattern that reduces coupling between components by making them communicate indirectly through a mediator object. It's perfect for breaking circular dependencies!
Let me walk you through how I implemented this pattern in a Kotlin/Koin project:
Step 1: Create a Mediator Interface
First, I defined an interface that declares methods both repositories need from each other:
interface TokenUserMediator {
/**
* Invalidates the refresh token for a user.
*/
fun invalidateUserTokens(username: String): Pair<Boolean, String?>
/**
* Retrieves a user profile by username.
*/
fun getUserByUsername(username: String): Profile?
}
This interface serves as a contract that both repositories can depend on, breaking the direct dependency between them.
Step 2: Implement the Mediator
Next, I created an implementation that delegates calls to the appropriate repositories:
class TokenUserMediatorImpl : TokenUserMediator, KoinComponent {
// Use Koin's lazy injection
private val tokenRepository: TokenRepository by inject()
private val usersRepository: UsersRepository by inject()
override fun invalidateUserTokens(username: String): Pair<Boolean, String?> {
return tokenRepository.invalidateUserTokens(username)
}
override fun getUserByUsername(username: String): Profile? {
return usersRepository.getUserByUsername(username)
}
}
The magic here is using Koin's inject()
function which lazily resolves dependencies only when the methods are called, not when the mediator is constructed. This breaks the initialization deadlock!
Step 3: Update Your Repositories
Now, I modified both repositories to depend on the mediator interface instead of directly on each other:
// TokenRepositoryImpl now depends on the mediator
class TokenRepositoryImpl(
private val mediator: TokenUserMediator
): TokenRepository {
// ...existing code...
override fun extractUserFromToken(call: ApplicationCall): Profile? {
// ...when we need user info...
return mediator.getUserByUsername(username)
}
}
// UsersRepositoryImpl also depends on the mediator
class UsersRepositoryImpl(
private val mediator: TokenUserMediator
) : UsersRepository {
// ...existing code...
override fun deleteUser(username: String): Pair<Boolean, String?> {
// ...when we need to invalidate tokens...
mediator.invalidateUserTokens(username)
// ...
}
}
Step 4: Wire It Up in Your DI Container
Finally, I updated the Koin module to register the mediator first, then the repositories:
val appModule = module {
// Register the mediator to break circular dependencies
single<TokenUserMediator> { TokenUserMediatorImpl() }
// Repositories that use the mediator
single<TokenRepository> { TokenRepositoryImpl(get()) }
single<UsersRepository> { UsersRepositoryImpl(get()) }
// Other components...
}
How It Works: Breaking the Loop
Let's visualize what we've done:
Before (circular dependency):
TokenRepository ──────► UsersRepository
▲ │
└──────────────────────┘
After (with mediator):
TokenUserMediator
(Interface)
▲ ▲
/ \
/ \
TokenRepository UsersRepository
▲ ▲
\ /
\ /
TokenUserMediatorImpl
When the application starts:
- Koin creates the
TokenUserMediator
implementation first - Then it creates both repositories, injecting the mediator into them
- When repository methods are called, the mediator's methods handle the communication
- The mediator lazily resolves the actual repositories only when needed
The Benefits: Why You Should Care
This pattern isn't just about fixing errors - it offers several advantages:
Clean Architecture: Your code becomes more modular and follows the Dependency Inversion Principle.
Easier Testing: You can test components in isolation by mocking the mediator.
Improved Maintainability: Adding new interactions between repositories becomes simpler - just add methods to the mediator.
Scalability: This pattern works well as your codebase grows, keeping dependencies manageable.
Alternative Approaches
While I found the Mediator Pattern to be the most elegant solution, there are other ways to tackle circular dependencies:
Service Locator: Similar to the mediator but more generic, allowing components to look up dependencies.
Extract Interface: Create interfaces for both components and depend on those instead.
Setter Injection: Use constructor injection for one dependency and setter injection for the other.
Refactoring: Sometimes the best solution is to restructure your code to eliminate the circular dependency entirely.
Real-World Implementation
In my project, this solution resolved a nasty StackOverflowError
that was preventing the application from starting. The codebase became more maintainable, and I could add new features without worrying about circular dependencies.
Here's what the actual implementation looked like in my Kotlin backend:
// TokenRepositoryImpl
override fun extractUserFromToken(call: ApplicationCall): Profile? {
return try {
val authorizationHeader = call.request.headers[HttpHeaders.Authorization]
if (authorizationHeader.isNullOrBlank() || !authorizationHeader.startsWith("Bearer ")) {
logMessage("No Authorization header found or invalid format")
return null
}
val accessToken = authorizationHeader.removePrefix("Bearer ").trim()
val username = getUsernameFromToken(accessToken)
if (username != null) {
return mediator.getUserByUsername(username) // Using the mediator!
}
null
} catch (e: Exception) {
logMessage("Error extracting user from token: ${e.message}", LogType.ERROR)
null
}
}
// UsersRepositoryImpl
override fun completePasswordReset(token: String, newPassword: String): Pair<Boolean, String?> {
return transaction {
try {
// ... validation logic ...
mediator.invalidateUserTokens(username) // Using the mediator!
// ... rest of the implementation ...
Pair(true, null)
} catch (e: Exception) {
Pair(false, "Error completing password reset: ${e.message}")
}
}
}
Conclusion
The mediator pattern is a powerful tool for breaking circular dependencies in your Kotlin applications. While it does introduce an extra layer of abstraction, the benefits in terms of cleaner architecture and avoiding runtime errors make it well worth the effort.
Next time you encounter a circular dependency problem, give the mediator pattern a try. Your codebase (and your sanity) will thank you!
Have you faced circular dependency issues in your projects? What solutions have you tried? Let me know in the comments!
Code examples in this post are from a real-world Kotlin backend application using Koin for dependency injection. The pattern can be applied in similar ways to other languages and frameworks.