Micronaut Multitenancy

Help with common tasks such as tenant resolution for multi-tenancy aware Micronaut applications.

Version: 5.5.1-SNAPSHOT

1 Release History

For this project, you can find a list of releases (with release notes) here:

2 Introduction

Multi-Tenancy, as it relates to software development, is when a single instance of an application is used to service multiple clients (tenants) in a way that each tenant’s data is isolated from the other.

2.1 Installation

To use the Micronaut’s multitenancy capabilities you must have the multitenancy dependency on your classpath.

implementation("io.micronaut.multitenancy:micronaut-multitenancy")
<dependency>
    <groupId>io.micronaut.multitenancy</groupId>
    <artifactId>micronaut-multitenancy</artifactId>
</dependency>

2.2 Built-In Tenant Resolvers

A common requirement for supporting Multi-tenancy is the ability to resolve the current tenant. Micronaut ships with the following built-in TenantResolvers:

name

description

CookieTenantResolver

Resolves the current tenant from an HTTP cookie. See CookieTenantResolver Configuration Properties.

FixedTenantResolver

Resolves against a fixed tenant id. See FixTenantResolver Configuration Properties.

HttpHeaderTenantResolver

Resolves the current tenant from the request HTTP Header. See FixTenantResolver Configuration Properties.

PrincipalTenantResolver

Resolves the current tenant from the authenticated username. See PrincipalTenantResolver Configuration Properties.

SessionTenantResolver

Resolves the current tenant from the HTTP session. See SessionTenantResolver Configuration Properties.

SubdomainTenantResolver

Resolves the tenant id from the subdomain. See SubdomainTenantResolver Configuration Properties.

SystemPropertyTenantResolver

Resolves the tenant id from a system property. See SystemPropertyTenantResolver Configuration Properties.

2.3 Tenant Binding

To access the resolved tenant, you can bind Tenant as a controller method parameter.

package io.micronaut.multitenancy;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;

import java.io.Serializable;

@Controller("/tenant")
class TenantBindingController {

    @Secured(SecurityRule.IS_ANONYMOUS)
    @Produces(MediaType.TEXT_PLAIN)
    @Get
    Serializable echoTenant(Tenant tenant) {
        return tenant.id();
    }
}
package io.micronaut.multitenancy

import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Produces
import io.micronaut.security.annotation.Secured
import io.micronaut.security.rules.SecurityRule

@Controller("/tenant")
class TenantBindingController {

    @Secured(SecurityRule.IS_ANONYMOUS)
    @Produces(MediaType.TEXT_PLAIN)
    @Get
    Serializable echoTenant(Tenant tenant) {
        tenant.id()
    }
}
package io.micronaut.multitenancy

import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Produces
import io.micronaut.security.annotation.Secured
import io.micronaut.security.rules.SecurityRule
import java.io.Serializable

@Controller("/tenant")
internal class TenantBindingController {
    @Secured(SecurityRule.IS_ANONYMOUS)
    @Produces(MediaType.TEXT_PLAIN)
    @Get
    fun echoTenant(tenant: Tenant): Serializable = tenant.id()
}

2.4 Tenant Request Attribute

The module ships a filter which resolves the current tenant and sets it as a request attribute named tenantIdentifier.

The following global configuration options are available for the TenantResolverFilter:

🔗
Table 1. Configuration Properties for TenantResolverFilterConfigurationProperties
Property Type Description

micronaut.multitenancy.filter.enabled

boolean

Whether TenantResolverFilter should be enabled. Default value (true).

micronaut.multitenancy.filter.regex-pattern

java.lang.String

Tenant Resolver filter processes only request paths matching this regular expression. Default Value: "^.*$"

2.5 Tenant Expression Language

To allow using the current request tenant Id in the Micronaut Expression Language, you need to include the multitenancy-annotation as an annotation processor dependency.

annotationProcessor("io.micronaut.multitenancy:micronaut-multitenancy-annotation")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.multitenancy</groupId>
        <artifactId>micronaut-multitenancy-annotation</artifactId>
    </path>
</annotationProcessorPaths>

For Kotlin, add the micronaut-multitenancy-annotation dependency in kapt or ksp scope, and for Groovy add micronaut-multitenancy-annotation in compileOnly scope.
@Controller
public class TenantCheckingSecuredController {

    @Get
    @Produces(MediaType.TEXT_PLAIN)
    @Secured("#{ tenantId == 'allowed' }") // (1)
    public String index() {
        return "Hello World";
    }
}
@Controller
class TenantCheckingSecuredController {

    @Get
    @Produces(MediaType.TEXT_PLAIN)
    @Secured("#{ tenantId == 'allowed' }") // (1)
    String index() {
        "Hello World"
    }
}
@Controller
class TenantCheckingSecuredController {

    @Get
    @Produces(MediaType.TEXT_PLAIN)
    @Secured("#{ tenantId == 'allowed' }") // (1)
    fun index() = "Hello World"
}
1 If tenantId (provided by the configured TenantResolver) is equal to 'allowed' then allow the request.

2.6 Subdomain Tenant Resolver

The SubdomainTenantResolver does not support every domain suffix. For example, it does not support second-level domains. However, Micronaut Multi-tenancy ships with other implementations, PublicSuffixListSubdomainTenantResolver and InternetDomainNameSubdomainTenantResolver, which support every domain suffix but require extra dependencies.

2.6.1 Public Suffix List Subdomain Tenant Resolver

To use it in addition to the micronaut-multitenancy dependency, you need to the Public Suffix List API dependency to your classpath:

implementation("de.malkusch.whois-server-list:public-suffix-list")
<dependency>
    <groupId>de.malkusch.whois-server-list</groupId>
    <artifactId>public-suffix-list</artifactId>
</dependency>

2.6.2 Guava Subdomain Tenant Resolver

To use it, in addition to the micronaut-multitenancy dependency, you need to add the guava dependency to your classpath.

2.7 Tenant Propagation

Micronaut supports tenant propagation. As an example, take the following scenario:

multitenancy

You want incoming requests to the gateway microservice to resolve the tenant id via subdomain. However, you want your requests to other internal microservices to include the tenant Id as an HTTP Header.

Your configuration in the gateway microservice will look like:

micronaut.multitenancy.propagation.enabled=true
micronaut.multitenancy.propagation.service-id-regex=catalogue
micronaut.multitenancy.tenantresolver.subdomain.enabled=true
micronaut.multitenancy.tenantwriter.httpheader.enabled=true
micronaut:
  multitenancy:
    propagation:
      enabled: true
      service-id-regex: 'catalogue'
    tenantresolver:
      subdomain:
        enabled: true
    tenantwriter:
      httpheader:
        enabled: true
[micronaut]
  [micronaut.multitenancy]
    [micronaut.multitenancy.propagation]
      enabled=true
      service-id-regex="catalogue"
    [micronaut.multitenancy.tenantresolver]
      [micronaut.multitenancy.tenantresolver.subdomain]
        enabled=true
    [micronaut.multitenancy.tenantwriter]
      [micronaut.multitenancy.tenantwriter.httpheader]
        enabled=true
micronaut {
  multitenancy {
    propagation {
      enabled = true
      serviceIdRegex = "catalogue"
    }
    tenantresolver {
      subdomain {
        enabled = true
      }
    }
    tenantwriter {
      httpheader {
        enabled = true
      }
    }
  }
}
{
  micronaut {
    multitenancy {
      propagation {
        enabled = true
        service-id-regex = "catalogue"
      }
      tenantresolver {
        subdomain {
          enabled = true
        }
      }
      tenantwriter {
        httpheader {
          enabled = true
        }
      }
    }
  }
}
{
  "micronaut": {
    "multitenancy": {
      "propagation": {
        "enabled": true,
        "service-id-regex": "catalogue"
      },
      "tenantresolver": {
        "subdomain": {
          "enabled": true
        }
      },
      "tenantwriter": {
        "httpheader": {
          "enabled": true
        }
      }
    }
  }
}

In the catalogue microservice the configuration will look like:

micronaut.multitenancy.tenantresolver.httpheader.enabled=true
micronaut:
  multitenancy:
    tenantresolver:
      httpheader:
        enabled: true
[micronaut]
  [micronaut.multitenancy]
    [micronaut.multitenancy.tenantresolver]
      [micronaut.multitenancy.tenantresolver.httpheader]
        enabled=true
micronaut {
  multitenancy {
    tenantresolver {
      httpheader {
        enabled = true
      }
    }
  }
}
{
  micronaut {
    multitenancy {
      tenantresolver {
        httpheader {
          enabled = true
        }
      }
    }
  }
}
{
  "micronaut": {
    "multitenancy": {
      "tenantresolver": {
        "httpheader": {
          "enabled": true
        }
      }
    }
  }
}

To propagate the tenant you will need to write the resolved tenant ID to the outgoing requests.

Currently, Micronaut ships with two built-in implementations for TenantWriter:

name

description

CookieTenantWriter

Writes the current tenant to a Cookie in your outgoing requests. See CookieTenantWriter Configuration Properties.

HttpHeaderTenantWriter

Writes the current tenant to a HTTP Header. See HttpHeaderTenantWriter Configuration Properties.

3 Multi-Tenancy GORM

GORM supports Multi-tenancy and integrates with Micronaut Multi-tenancy support.

See the Micronaut Groovy module for more information.

4 Repository

You can find the source code of this project in this repository: