An Elegant Way to Solve Multi-Tenancy
Tags: kotlin, architecture
Published: 2024-01-30 16:14:46

_In this article, I will show you an elegant way to introduce multi-tenancy in an already established code base for a Kotlin/Koin stack. And while the examples are specific to this stack, the general idea should be applicable to any dependency injection framework that supports qualifiers._

---

## History

Our product initially started below the radar to support the migration of MediaMarktSaturn's webshop to the new platform. Back then, we had to replace an existing legacy promotion system by the beginning of the Black Friday season, which meant that we already had a fixed (business) scope _and_ a fixed deadline.

Naturally, we had to cut short on some of the more technical aspects of our code base. One of that aspects was multi-tenancy, in our case different countries and sales lines - Media Markt Germany would be a different tenant than Saturn Germany or Media Markt Austria.

Of course, we knew that we had to deal with multiple tenants _at some point_ but since the whole migration targeted mediamarkt.de only, we could easily postpone that topic until after the first go-live. We already included the relevant parts in our data model and in our API, though, so that we wouldn't have to perform a data migration and consumers of our system could keep their existing integrations.

For our implementation we used the much easier "ignorance" strategy, i.e. we didn't deal with tenants anywhere - and in the (few) places where we _needed_ to work with a tenant, we simply hardcoded it to Media Markt Germany.

## Initial Setup

We are using a pure Kotlin reactive stack with [Ktor](https://ktor.io/) as web framework and [Koin](https://insert-koin.io/) for dependency injection and application configuration. Our product manages coupons and calculates discounts for customers, i.e assesses their baskets with respect to pre-configured promotion rules.

In its essence, our setup looked like this:

```kotlin
val serviceModule = module {
    single { OutletRepository() }
    single { CalculationService() }
    single { PromotionRepository() }
    single { CouponRepository() }
}
```

You can think of a `module` as a space to collect all your Koin-managed components, similar to "beans" in Spring or CDI. `single` declares a singleton component that will be instantiated only once and re-used for every injection point.

In our routing layer we receive basket assessment requests and forward them to our main service, the `CalculationService`:

```kotlin
routing {
    val calculationService: CalculationService = get()

    get("basket-assessments") {
        val request = call.receive<CalculationRequest>()
        call.respond(calculationService.calculateBasket(request))
    }
}
```

The `CalculationService` then fetches active promotions from the `PromotionRepository` and applies coupons using a `CouponRepository`. Naturally, this setup has different requirements to multi-tenancy: While coupons and active promotions differ between country and sales line, the core calculation logic will always stay the same and also outlet master data will be identical across all tenants. Coupons are identified by their code and thus even need to be separated on persistence level, so that multiple tenants can use identical codes.

Inside our `CalculationService` we use `get` to let Koin eagerly resolve and inject the managed instance(s) of other services:

```kotlin
class CalculationService {
    private val promotionRepository: PromotionRepository = get()
    private val couponRepository: CouponRepository = get()
    private val outletRepository: OutletRepository = get()
    ...
}
```

## From Simple...

When we eventually added support for multi-tenancy, we already had an established code base, so we were facing a few challenges:

- We couldn't afford (and didn't want) to rebuild everything from scratch
- We had lots of functions that required to know which tenant they were working with
- We also had lots of tests that didn't know anything about tenants yet

So we were looking for a solution that would allow us to keep as much of our current implementation as possible while at the same time didn't force us to introduce changes everywhere. We decided to separate all data on database level already so that our tenants could work independently from each other - I've already mentioned coupon codes above, but this can be seen as a good practice in general.

The simple approach was to determine a tenant from the request, and then add a `tenant` parameter to each subsequent function call. Of course, that wasn't exactly... _elegant_.

We had some places that experimented with the [`CoroutineContext`](https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html) to transfer tenant information but there were still a lot of places (including tests) that needed to be adapted, and back then we were just starting to understand how coroutines work. Also, we had to rely on the presence of a tenant during runtime, and actually wanted to have a bit more compile-time safety.

## ...to Elegant

So instead, we opted for "tenant-aware" services. We divided our existing service implementations into those that would provide common functionality and those that needed a tenant. For the latter we introduced a `TenantAware` interface with a single property:

```kotlin
interface TenantAware {
    val tenant: Tenant
}
```

All services that needed a tenant would now implement this interface and receive a `tenant` in their constructor. So our module configuration changed to:

```kotlin
val serviceModule = module {
    single { OutletRepository() }
    SupportedTenant.values().forEach { tenant ->
        single(named(tenant.id)) { CalculationService(tenant) }
        single(named(tenant.id)) { PromotionRepository(tenant) }
        single(named(tenant.id)) { CouponRepository(tenant) }
    }
}
```

This way, we had e.g. an instance of `PromotionRepository` for MediaMarkt Germany and one for Saturn Germany, and within that instance we had reliable access to the `tenant` property whenever we needed it - for example, when referring to other tenant-aware services that would now resolve the tenant-specific instance from Koin:

```kotlin
class CalculationService(override val tenant: Tenant) : TenantAware {
    private val promotionRepository: PromotionRepository = get(named(tenant.id))
    private val couponRepository: CouponRepository = get(named(tenant.id))
    private val outletRepository: OutletRepository = get()
    ...
}
```

This allowed us to limit all new tenant functionality to our routing layer, and we didn't have to change anything in our business code because every tenant-aware instance would only ever talk to other instances for the same tenant:

```kotlin
routing {
    get("basket-assessments") {
        val request = call.receive<CalculationRequest>()
        val calculationService = get<CalculationService>(named(request.tenant.id))
        call.respond(calculationService.calculateBasket(request))
    }
}
```

And by adding a tenant suffix to our database collection (we are using [MongoDB](https://www.mongodb.com/)) we could easily achieve data separation in our persistence as well:

```kotlin
class PromotionRepository(override val tenant: Tenant) : TenantAware {
    val collectionName = "promotions.${tenant.id}"
    ...
}
```

In our tests we only had to adapt our injection points and didn't need to touch any of the actual test methods - voila! If needed, we could easily test multiple tenants by simply injecting different instances of a service.

## Extension Functions

As last polish, we used Kotlin's extension functions to simplify our module definition and dependency resolution:

```kotlin
val Tenant.qualifier
    get() = named(id)

inline fun <reified T> Module.singleByTenant(noinline block: Scope.(Tenant) -> T) =
    SupportedTenant.values().map { tenant ->
        single(tenant.qualifier) { block(tenant) }
    }

inline fun <reified T : Any> get(tenant: Tenant) = get<T>(tenant.qualifier)
```

```kotlin
val serviceModule = module {
    single { OutletRepository() }
    singleByTenant { CalculationService(it) }
    singleByTenant { PromotionRepository(it) }
    singleByTenant { CouponRepository(it) }
}

class CalculationService(override val tenant: Tenant) : TenantAware {
    private val promotionRepository: PromotionRepository = get(tenant)
    private val couponRepository: CouponRepository = get(tenant)
    private val outletRepository: OutletRepository = get()
    ...
}

routing {
    get("basket-assessments") {
        val request = call.receive<CalculationRequest>()
        val calculationService = get<CalculationService>(request.tenant)
        call.respond(calculationService.calculateBasket(request))
    }
}
```

During the initial design we spent some days tweaking our multi-tenancy implementation but since then we've had no issues and are still using it without major changes.

And with our meanwhile grown understanding of coroutines, we have also been able to use our `TenantAware` interface for elegant tenant-aware authorization:

```kotlin
suspend inline fun <T> TenantAware.requireClaim(
    claim: Claim,
    block: () -> T
): T {
    // Check if current user has required `claim` for current `tenant`
    ...
}

class CouponService(override val tenant: Tenant) : TenantAware {
    fun createCoupon() = requireClaim(Claim.COUPON_CREATE) {
        ...
    }
}

```

Tell me what you think and especially, if you see even more potential for improving!

---

**_get to know us 👉 [https://mms.tech](https://mms.tech) 👈_**