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.StackOverflowError during startKoin or 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. 1.Identify the circular dependency chain:
  2. 2.```kotlin
  3. 3.// The problematic setup:
  4. 4.module {
  5. 5.single<UserService> { UserService(get()) } // needs OrderService
  6. 6.single<OrderService> { OrderService(get()) } // needs UserService
  7. 7.}

// Fix: extract shared logic to a third component module { single<UserOrderCoordinator> { UserOrderCoordinator() } single<UserService> { UserService(get<UserOrderCoordinator>()) } single<OrderService> { OrderService(get<UserOrderCoordinator>()) } } ```

  1. 1.Use lazy injection to break the cycle:
  2. 2.```kotlin
  3. 3.module {
  4. 4.single<UserService> { UserService(get()) }
  5. 5.// Use lazy resolution: the dependency is resolved when first accessed
  6. 6.single<OrderService> { OrderService(get()) }
  7. 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. 1.Use Koin's lazy injection syntax:
  2. 2.```kotlin
  3. 3.class OrderService : KoinComponent {
  4. 4.// Lazy: resolved only when first used
  5. 5.private val userService: UserService by lazy { get() }

fun processOrder(orderId: String) { val user = userService.getCurrentUser() // ... } } ```

  1. 1.Use interface segregation to break the cycle:
  2. 2.```kotlin
  3. 3.// Instead of both services depending on each other,
  4. 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. 1.Use events instead of direct dependencies:
  2. 2.```kotlin
  3. 3.// Instead of OrderService calling UserService directly:
  4. 4.class EventBus {
  5. 5.private val _events = MutableSharedFlow<AppEvent>()
  6. 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 single for shared dependencies, factory for 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