Tenant-Scoped Components

A multi-tenant application usually has its own tenant-scoped resources: a read-model store, a SQL datasource, or any service that must not mix data between tenants. A tenant-scoped component lets you register such a resource once and have the correct tenant’s instance injected into your message handling methods, so a handler never has to look up the tenant itself. The resource itself stays passive: you describe how it is built for a tenant, and the framework holds one instance per tenant and injects the matching one into the handler.

A tenant-scoped component is described by two types:

  • TenantComponentFactory<T> builds (and cleans up) the instance of your component for a single tenant.

  • TenantComponentProvider<T> holds one instance of the component type T per tenant, creating each instance lazily through the factory on first use.

Each provider is generic over a single component type. To expose several tenant-scoped types, register one provider per type. Each provider is matched to handler parameters by its component type.

Registering a tenant-scoped component

Implement a TenantComponentFactory describing how the component is built for a tenant, and register the provider obtained through TenantComponentProvider.withFactory.

  • Declarative - Configuration API

  • Autodetected - Spring Boot

Register the provider as a component on your MessagingConfigurer, passed in from the place it is created:

public void registerTenantComponents(MessagingConfigurer configurer) {
    configurer.componentRegistry(registry ->
        registry.registerComponent(
                TenantComponentProvider.class,                                     (1)
                config -> TenantComponentProvider.withFactory(
                        CourseStatisticsStore.class,                               (2)
                        tenant -> new InMemoryCourseStatisticsStore(tenant.tenantId())
                )
        )
    );
}
1 Register the provider under the TenantComponentProvider type so it is discovered during parameter resolution.
2 The component type, used to match the provider against handler parameters. The lambda is the TenantComponentFactory building the tenant’s instance.

Expose the provider as a bean of type TenantComponentProvider:

@Bean
public TenantComponentProvider<CourseStatisticsStore> courseStatisticsStoreProvider() { (1)
    return TenantComponentProvider.withFactory(
            CourseStatisticsStore.class,                                                 (2)
            tenant -> new InMemoryCourseStatisticsStore(tenant.tenantId())
    );
}
1 The bean type is TenantComponentProvider, which is how it is discovered during parameter resolution.
2 The component type, used to match the provider against handler parameters. The lambda is the TenantComponentFactory building the tenant’s instance.

Message handlers can then declare the component type as a parameter.

The university demo builds its providers in TenantComponents and registers them from the UniversityConfiguration of its declarative and Spring Boot variants.

Injecting the tenant’s instance into a handler

Declare an @TenantScoped-annotated parameter of the component type on a message handling method. During handling, the parameter is resolved to the instance belonging to the tenant of the message being handled.

@EventHandler
public void on(CourseCreated event, @TenantScoped CourseStatisticsStore store) { (1)
    store.save(new CourseStatistics(event.courseId()));
}
1 store is the CourseStatisticsStore instance of the current tenant. The @TenantScoped annotation marks it as tenant scoped.

The tenant whose instance is injected is the tenant resolved for the message being handled. See Tenant resolution for how the tenant of a message is resolved and how to customize that resolution. When no tenant can be resolved for the message, injecting the parameter fails with a TenantNotResolvedException, so a tenant-scoped resource is never created for an unresolved tenant.

The demo injects tenant-scoped components into all three handler kinds: a command handler, an event handler and a query handler.

Registering multiple component types

An application can register several tenant-scoped components, one provider per type. Each provider is matched to a handler parameter by its component type. Register a single provider per component type: registering two providers for the same type, or a parameter type that matches several providers, results in an AxonConfigurationException.

  • Declarative - Configuration API

  • Autodetected - Spring Boot

Register each provider under its own name:

configurer.componentRegistry(registry -> registry
    .registerComponent(TenantComponentProvider.class, "courseStatisticsStore", (1)
            config -> TenantComponentProvider.withFactory(
                    CourseStatisticsStore.class,
                    tenant -> new InMemoryCourseStatisticsStore(tenant.tenantId())))
    .registerComponent(TenantComponentProvider.class, "reportingDataSource",   (2)
            config -> TenantComponentProvider.withFactory(
                    DataSource.class,
                    tenant -> buildDataSource(tenant)))
);
1 A provider for the CourseStatisticsStore read model.
2 A second provider, for a per-tenant reporting DataSource, registered under a different name.

Declare one bean per component type. The bean name distinguishes them:

@Bean
public TenantComponentProvider<CourseStatisticsStore> courseStatisticsStore() {  (1)
    return TenantComponentProvider.withFactory(
            CourseStatisticsStore.class,
            tenant -> new InMemoryCourseStatisticsStore(tenant.tenantId()));
}

@Bean
public TenantComponentProvider<DataSource> reportingDataSource() {              (2)
    return TenantComponentProvider.withFactory(
            DataSource.class,
            tenant -> buildDataSource(tenant));
}
1 A provider for the CourseStatisticsStore read model.
2 A second provider, for a per-tenant reporting DataSource.

Lifecycle and cleanup

Providers follow the tenant lifecycle automatically. A tenant’s component is created the first time a handler for that tenant needs it, cached for later messages, and destroyed through TenantComponentFactory#destroy when the tenant is removed.

The default destroy closes components that implement AutoCloseable, which covers pooled datasource implementations and other connection-backed resources. Override it to release other resources or to add custom shutdown logic.

TenantComponentFactory<CourseStatisticsStore> factory = new TenantComponentFactory<>() {

    @Override
    public CourseStatisticsStore create(TenantDescriptor tenant) {
        return new JdbcCourseStatisticsStore(dataSourceFor(tenant));
    }

    @Override
    public void destroy(TenantDescriptor tenant, CourseStatisticsStore store) {
        store.flush();
        store.close();
    }
};