Tenant-Aware Dead-Letter Queues
The dead-letter queue is tenant-aware when multi-tenancy is enabled. The framework provides one routing queue and registry; each operation resolves the tenant from its ProcessingContext and delegates to that tenant’s concrete queue.
Enable the dead-letter queue for the processor as described in Dead-Letter Queue. Then provide a TenantAwareSequencedDeadLetterQueueFactory that creates the concrete JPA, JDBC, or in-memory queue for a tenant. The factory should obtain tenant-specific persistence resources from your application’s tenant-scoped components.
Providing tenant-specific queues
The ordinary SequencedDeadLetterQueueFactory is not replaced by your application code. Multi-tenancy installs the routing adapter automatically via enhancer and uses the tenant-aware factory you configure.
In Spring Boot, register the TenantAwareSequencedDeadLetterQueueFactory as a bean and enable the processor’s DLQ property. The bean is the only additional multi-tenancy DLQ integration point; your factory implementation still needs access to the tenant-specific datasource or entity manager it uses.
The following examples configure a JDBC queue. They assume that the application already registers a tenant-scoped DataSource provider as described in Tenant-scoped components.
-
Declarative configuration
-
Spring Boot
public void configure(MessagingConfigurer configurer) {
configurer.componentRegistry(registry -> registry.registerComponent(
TenantAwareSequencedDeadLetterQueueFactory.class,
configuration -> (tenant, processorName, queueConfiguration) -> createQueue(
tenant, processorName, queueConfiguration
))
);
}
private JdbcSequencedDeadLetterQueue<EventMessage> createQueue(
TenantDescriptor tenant,
String processorName,
Configuration configuration
) {
TenantComponentProvider<DataSource> dataSourceProvider =
TenantComponentProviderUtil.find(configuration, DataSource.class)
.orElseThrow();
JdbcTransactionalExecutorProvider executor =
new JdbcTransactionalExecutorProvider(dataSourceProvider.componentFor(tenant));
return JdbcSequencedDeadLetterQueue.<EventMessage>builder()
.processingGroup(processorName)
.transactionalExecutorProvider(ignored -> executor.getTransactionalExecutor(
null
))
.eventConverter(configuration.getComponent(EventConverter.class))
.genericConverter(configuration.getComponent(GeneralConverter.class))
.build();
}
@Bean
public TenantAwareSequencedDeadLetterQueueFactory tenantAwareDeadLetterQueueFactory() {
return (tenant, processorName, queueConfiguration) -> createQueue(tenant, processorName, queueConfiguration);
}
private JdbcSequencedDeadLetterQueue<EventMessage> createQueue(
TenantDescriptor tenant,
String processorName,
Configuration configuration
) {
TenantComponentProvider<DataSource> dataSourceProvider =
TenantComponentProviderUtil.find(configuration, DataSource.class)
.orElseThrow();
JdbcTransactionalExecutorProvider executor =
new JdbcTransactionalExecutorProvider(dataSourceProvider.componentFor(tenant));
return JdbcSequencedDeadLetterQueue.<EventMessage>builder()
.processingGroup(processorName)
.transactionalExecutorProvider(ignored -> executor.getTransactionalExecutor(
null
))
.eventConverter(configuration.getComponent(EventConverter.class))
.genericConverter(configuration.getComponent(GeneralConverter.class))
.build();
}
Replaying a tenant’s dead letters
When replaying dead letters, create a unit of work by retrieving the UnitOfWorkFactory from the configuration and provide the tenant explicitly in its ProcessingContext as a resource.
The routing queue resolves its tenant before it reads a dead letter, so it cannot derive the tenant from the dead letter’s captured context.
Set TenantDescriptor.RESOURCE_KEY on the unit of work’s context and call the context-aware overload of processAny or process.
For example, the following application service replays the oldest sequence for a tenant:
import io.axoniq.framework.messaging.deadletter.SequencedDeadLetterProcessor;
import io.axoniq.framework.messaging.multitenancy.api.TenantDescriptor;
import org.axonframework.messaging.core.unitofwork.UnitOfWorkFactory;
import org.axonframework.messaging.eventhandling.EventMessage;
import java.util.concurrent.CompletableFuture;
public class TenantDeadLetterReplay {
private final SequencedDeadLetterProcessor<EventMessage> processor;
private final UnitOfWorkFactory uowFactory;
public TenantDeadLetterReplay(SequencedDeadLetterProcessor<EventMessage> processor,
UnitOfWorkFactory uowFactory) {
this.processor = processor;
this.uowFactory = uowFactory;
}
public CompletableFuture<Boolean> processAnyLetterFor(String tenantId) {
return uowFactory.create().executeWithResult(context -> {
context.putResource(TenantDescriptor.RESOURCE_KEY, TenantDescriptor.tenantWithId(tenantId));
return processor.processAny(context);
});
}
}
Ensure the tenant resource is set for every replay request, including requests initiated by an administrative API or scheduled job.