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 typeTper 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();
}
};