Introduction
Koin detects circular dependencies at runtime when resolving the dependency graph. If Service A requires Service B, and Service B requires Service A (directly or through a chain), Koin enters infinite recursion and crashes with a StackOverflowError. Unlike compile-time DI frameworks like Dagger, Koin discovers these cycles only at runtime.
Symptoms
java.lang.StackOverflowErrorduringstartKoinor first dependency resolution- App crashes immediately on launch
- Stack trace shows repeated Koin resolution calls
- Error occurs after adding a new dependency
- Works until a specific module is loaded
Example error:
``
java.lang.StackOverflowError: stack size 8192KB
at org.koin.core.instance.InstanceFactory.create(InstanceFactory.kt:52)
at org.koin.core.instance.SingleInstanceFactory.create(SingleInstanceFactory.kt:42)
at org.koin.core.instance.SingleInstanceFactory$get$1.invoke(SingleInstanceFactory.kt:54)
... (repeats between UserService and OrderService)
Common Causes
- Two services reference each other directly
- Event bus or mediator that is injected by both parties
- Repository depends on service that depends on the same repository
- Callback/listener pattern creating mutual dependency
- Module A depends on Module B which depends on Module A
Step-by-Step Fix
- 1.Identify the circular dependency chain:
- 2.```kotlin
- 3.// The problematic setup:
- 4.module {
- 5.single<UserService> { UserService(get()) } // needs OrderService
- 6.single<OrderService> { OrderService(get()) } // needs UserService
- 7.}
// Fix: extract shared logic to a third component module { single<UserOrderCoordinator> { UserOrderCoordinator() } single<UserService> { UserService(get<UserOrderCoordinator>()) } single<OrderService> { OrderService(get<UserOrderCoordinator>()) } } ```
- 1.Use lazy injection to break the cycle:
- 2.```kotlin
- 3.module {
- 4.single<UserService> { UserService(get()) }
- 5.// Use lazy resolution: the dependency is resolved when first accessed
- 6.single<OrderService> { OrderService(get()) }
- 7.}
class OrderService( private val userServiceProvider: () -> UserService ) { private val userService by lazy { userServiceProvider() }
fun placeOrder(userId: String) { // UserService is resolved here, not during construction val user = userService.getUser(userId) // ... } } ```
- 1.Use Koin's lazy injection syntax:
- 2.```kotlin
- 3.class OrderService : KoinComponent {
- 4.// Lazy: resolved only when first used
- 5.private val userService: UserService by lazy { get() }
fun processOrder(orderId: String) { val user = userService.getCurrentUser() // ... } } ```
- 1.Use interface segregation to break the cycle:
- 2.```kotlin
- 3.// Instead of both services depending on each other,
- 4.// define interfaces that capture only what each needs
interface UserReader { fun getUser(id: String): User }
interface OrderNotifier { fun notifyOrderCreated(orderId: String) }
class UserService : UserReader { override fun getUser(id: String): User = ... }
class OrderService( private val userReader: UserReader // Only needs to read, not full UserService ) { fun createOrder(userId: String) { val user = userReader.getUser(userId) // ... } } ```
- 1.Use events instead of direct dependencies:
- 2.```kotlin
- 3.// Instead of OrderService calling UserService directly:
- 4.class EventBus {
- 5.private val _events = MutableSharedFlow<AppEvent>()
- 6.val events: SharedFlow<AppEvent> = _events
suspend fun publish(event: AppEvent) = _events.emit(event) }
class UserService(eventBus: EventBus) { init { CoroutineScope(Dispatchers.Default).launch { eventBus.events.collect { event -> if (event is OrderCreatedEvent) { // React to order creation } } } } } ```
Prevention
- Draw dependency graphs before adding new dependencies
- Use
singlefor shared dependencies,factoryfor stateful ones - Avoid bidirectional dependencies between services
- Use interfaces to reduce coupling between components
- Add unit tests that verify the Koin module can start:
startKoin { modules(appModule) } - Consider migrating to compile-time DI (Hilt/Dagger) for larger projects