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.