Tenant Management

Tenant management covers how tenants become known to the application and how the tenant of a message is resolved during processing. A tenant is represented by a TenantDescriptor, identified by its tenantId, which corresponds to an Axon Server context.

Enabling and disabling multi-tenancy

Multi-tenancy is active by default: with the axoniq-multi-tenancy module on the classpath, no code or configuration is needed to switch it on. In a Spring Boot application this is handled by auto-configuration. With the Configuration API the enhancers the module contributes apply on their own.

You may want to opt out anyway, for instance because the module is inherited transitively while the application runs single-tenant. After opting out, the application behaves as a single-tenant application.

  • Declarative - Configuration API

  • Autodetected - Spring Boot

Disable multi-tenancy on the configurer:

public void disableMultiTenancy(MessagingConfigurer configurer) {
    configurer.componentRegistry(MultiTenancyUtils::disable);
}

Because tenants are Axon Server contexts, multi-tenancy is only active while Axon Server is enabled, and stays inactive when axon.axonserver.enabled=false.

To switch it off entirely:

axon.multitenancy.enabled=false

Tenants are Axon Server contexts, so multi-tenancy cannot function without a connection to Axon Server. With the Configuration API, an application that carries the module without Axon Server configured fails when the tenant provider is resolved, pointing at both ways out: configure Axon Server, or opt out as shown above.

With multi-tenancy active, tenants are taken from your Axon Server contexts and the tenant of each message is resolved automatically. The sections below show how to customize which contexts become tenants and how the tenant of a message is resolved.

How tenants become known

Each tenant maps to an Axon Server context. With multi-tenancy active, the contexts of the connected Axon Server are discovered as tenants at startup, and the set is kept current at runtime: contexts added or removed on Axon Server become tenants that are added or removed in the running application, without further configuration.

Filtering which contexts are tenants

By default, every Axon Server context except the _admin context is treated as a tenant. To narrow the set, register a TenantConnectPredicate, for example to include only contexts matching a naming convention:

  • Declarative - Configuration API

  • Autodetected - Spring Boot

public void registerTenantFilter(MessagingConfigurer configurer) {
    configurer.componentRegistry(registry -> registry.registerComponent(
            TenantConnectPredicate.class,
            config -> tenant -> tenant.tenantId().startsWith("tenant-")));
}

Expose the predicate as a bean of type TenantConnectPredicate:

@Bean
public TenantConnectPredicate tenantConnectPredicate() {
    return tenant -> tenant.tenantId().startsWith("tenant-");
}

Tenant resolution

Tenant resolution determines, for each message, which tenant it belongs to. The resolved tenant determines which tenant’s tenant-scoped components a handler receives.

Resolution is performed by a TenantResolver, and the same resolved tenant is used throughout the handling of that message.

When no tenant can be resolved for a message, resolution fails with a TenantNotResolvedException, and the message is not handled for any tenant.

The default resolver

When multi-tenancy is active and no other resolver is registered, the default is a MetadataBasedTenantResolver. It reads the tenant identifier from the message metadata under the tenantId key. A message whose metadata does not contain that key cannot be resolved and fails with a TenantNotResolvedException.

Customizing tenant resolution

To read the tenant from a different metadata key, register a MetadataBasedTenantResolver with that key:

  • Declarative - Configuration API

  • Autodetected - Spring Boot

public void registerTenantResolverForKey(MessagingConfigurer configurer) {
    configurer.componentRegistry(registry -> registry.registerComponent(
            TenantResolver.class,
            config -> new MetadataBasedTenantResolver("tenant")));
}

Expose the resolver as a bean of type TenantResolver:

@Bean
public TenantResolver tenantResolver() {
    return new MetadataBasedTenantResolver("tenant");
}

To resolve the tenant from something other than metadata, register your own TenantResolver. It receives the message and the collection of known tenants, and returns the resolved TenantDescriptor, or throws a TenantNotResolvedException when it cannot:

  • Declarative - Configuration API

  • Autodetected - Spring Boot

public void registerCustomTenantResolver(MessagingConfigurer configurer) {
    TenantResolver resolver = new TenantResolver() {
        @Override
        public TenantDescriptor resolveTenant(Message message, Collection<TenantDescriptor> tenants) {
            String tenantId = message.metadata().get("x-tenant");            (1)
            if (tenantId == null) {
                throw new TenantNotResolvedException("Could not resolve a tenant for the message");
            }
            return TenantDescriptor.tenantWithId(tenantId);
        }

        @Override
        public Message attachTenant(Message message, TenantDescriptor tenant) {
            return message.andMetadata(Map.of("x-tenant", tenant.tenantId()));     (2)
        }
    };
    configurer.componentRegistry(registry -> registry.registerComponent(TenantResolver.class,
                                                                        config -> resolver));
}
1 Application-specific logic deriving the tenant identifier from the message.
2 The inverse: writes the tenant back under the same header, so it survives being dispatched elsewhere. See Propagating the tenant across a dispatch hop.

Expose your TenantResolver as a bean:

@Bean
public TenantResolver tenantResolver() {
    return new TenantResolver() {
        @Override
        public TenantDescriptor resolveTenant(Message message, Collection<TenantDescriptor> tenants) {
            String tenantId = message.metadata().get("x-tenant");            (1)
            if (tenantId == null) {
                throw new TenantNotResolvedException("Could not resolve a tenant for the message");
            }
            return TenantDescriptor.tenantWithId(tenantId);
        }

        @Override
        public Message attachTenant(Message message, TenantDescriptor tenant) {
            return message.andMetadata(Map.of("x-tenant", tenant.tenantId()));     (2)
        }
    };
}
1 Application-specific logic deriving the tenant identifier from the message.
2 The inverse: writes the tenant back under the same header, so it survives being dispatched elsewhere. See Propagating the tenant across a dispatch hop.

Propagating the tenant across a dispatch hop

A command or query dispatched from inside a handler generally does not name its own tenant: the tenant is only known through the message being handled. TenantResolver attaches that tenant onto the outgoing message through attachTenant, the inverse of resolving it, so it survives a distributed round trip and is available again once the dispatched message reaches its handler.

The default MetadataBasedTenantResolver does this out of the box: it writes the tenant identifier under the same metadata key it reads it from, so a command or query dispatched from within another tenant-owned handler, without naming a tenant of its own, stays attributed to that tenant with no further configuration.

A custom TenantResolver must implement both resolveTenant and attachTenant itself, to allow commands and queries to propagate their tenant information when dispatched from inside another handler. The custom resolver above already does this, writing the tenant back under the same header it reads it from.