Tenant-Scoped Components

A multi-tenant application usually has its own tenant-scoped resources: a read-model repository, 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.

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(
                        CourseStatsRepository.class,                               (2)
                        tenant -> new InMemoryCourseStatsRepository(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.

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

Injecting the tenant’s instance into a handler

Declare a 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, CourseStatsRepository repository) { (1)
    repository.save(new CourseStatistics(event.courseId()));
}
1 repository is the CourseStatsRepository instance of the current tenant.

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.

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.

Register each provider under its own name:

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

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<CourseStatsRepository> factory = new TenantComponentFactory<>() {

    @Override
    public CourseStatsRepository create(TenantDescriptor tenant) {
        return new JdbcCourseStatsRepository(dataSourceFor(tenant));
    }

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