Configuration

Minimal configuration is needed to get extension up and running.

Static tenants configuration

If you know list of contexts that you want your application to connect in advanced configure them coma separated in application.properties via following properties: axon.axonserver.contexts=tenant-context-1,tenant-context-2,tenant-context-3

Dynamic tenants configuration

If you don’t know tenants in advance and you plan to create them in runtime, you can define a predicate which will tell application to which contexts to connect to in runtime:

@Bean
public TenantConnectPredicate tenantFilterPredicate() {
    return context -> context.tenantId().startsWith("tenant-");
}

Note that in this case you need to remove axon.axonserver.contexts property.

Route message to specific tenant

By default, to route message to specific tenant you need to tag initial message that enters your system with metadata . This is done with meta-data helper, and you need to add tenant name to metadata with key TenantConfiguration.TENANT_CORRELATION_KEY.

message.andMetaData(Collections.singletonMap(TENANT_CORRELATION_KEY, "tenant-context-1")

Metadata needs to be added only to initial message that enters your system. Any message that is produced by consequence of initial message will have this metadata copied automatically using to CorrelationProvider.

Custom resolver

If you wish to disable default meta-data based routing define following property:

axon.multi-tenancy.use-metadata-helper=false

And define custom tenant resolver bean. For example following imaginary bean can use message payload to route message to specific tenant:

@Bean
public TargetTenantResolver<Message<?>> customTargetTenantResolver() {
    return (message, tenants) -> (1)
            TenantDescriptor.tenantWithId(
                    message.getPayload().getField("tenantName")
            );
}
1 First lambda parameter message represents message to be routed, while second parameter tenants represents list of currently registered tenants, if you wish to use is to route only to one of connected tenants.

Multi-tenant projections

If you wish to use distinct database to store projections and token store for each tenant, configure following bean:

@Bean
public Function<TenantDescriptor, DataSourceProperties> tenantDataSourceResolver() {
    return tenant -> {
        DataSourceProperties properties = new DataSourceProperties();
        properties.setUrl("jdbc:postgresql://localhost:5432/"+tenant.tenantId());
        properties.setDriverClassName("org.postgresql.Driver");
        properties.setUsername("postgres");
        properties.setPassword("postgres");
        return properties;
    };
}

Note that this works by using JPA multi-tenancy support, that means only SQL Databases are supported out of the box. If you wish to implement multi-tenancy for a different type of databases (for example, NoSQL) make sure that your projection database supports multi-tenancy. While in transaction you may find out which tenant owns transaction by calling: TenantWrappedTransactionManager.getCurrentTenant().

For more hints how to enable multi-tenancy for NoSQL databases check on how JPA SQL version is implemented

In this case Liquibase or Flyway will not be able to initialise schemas for dynamic data sources. Any datasource that you use needs to have pre-initialized schema.

Query update emitter

In order to correctly resolve right query update emitter inject update emitter in following style:

@EventHandler
public void on(Event event, QueryUpdateEmitter queryUpdateEmitter) {
  //queryUpdateEmitter will route updates to same tenant as event will be
  ...
}

Resetting projections

Resetting projections works a bit different because you have instanced of each event processor group for each tenant.

Reset specific tenant event processor group:

TrackingEventProcessor trackingEventProcessor =
        configuration.eventProcessingConfiguration()
                     .eventProcessor("com.demo.query-ep@tenant-context-1",
                                     TrackingEventProcessor.class)
                     .get();

Name of each event processor is: {even processor name}@{tenant name}

Access all tenant event processors by retrieving MultiTenantEventProcessor only. MultiTenantEventProcessor acts as a proxy Event Processor that references all tenant event processors.

Dead-letter queue

The configuration of a dead-letter queue is similar to a non-multi-tenant environment. The tenant will be resolved through the Message’s MetaData and routed to the corresponding DLQ. If you wish to have different enqueuing policies per tenant, you can use the MetaData from the dead letter message to determine to which tenant the message belongs to act accordingly.

Do note that processing dead letters from the queue is slightly different, as you need the specific tenant context to process dead-letter from.

To select the tenant for which you want to process a dead letter, you need to cast the SequencedDeadLetterProcessor to a MultiTenantDeadLetterProcessor. From the MultiTenantDeadLetterProcessor, you need to use the forTenant method to select the tenant-specific SequencedDeadLetterProcessor.

public class DlqManagement {

    private MultiTenantDeadLetterProcessor multiTenantDeadLetterProcessor;

    // Axon Framework's org.axonframework.config.Configuration
    public DlqManagement(Configuration configuration) {
        SequencedDeadLetterProcessor deadLetterProcessor = configuration.sequencedDeadLetterProcessor();
        this.multiTenantDeadLetterProcessor = (MultiTenantDeadLetterProcessor) deadLetterProcessor;
    }

    public void processDeadLetterSequenceForTenant(String tenantId,
                                                   Predicate<DeadLetter<? extends EventMessage<?>>> sequenceFilter) {
        multiTenantDeadLetterProcessor.forTenant(tenantId)
                                      .process(sequenceFilter);
    }
}

Here is a full example of a REST endpoint to retry dead letters for a specific tenant:

public class DlqManagementController {

    // Axon Framework's org.axonframework.config.Configuration
    private Configuration configuration;

    @PostMapping(path = "/retry-dlq")
    public void retryDLQ(@RequestParam String tenant, @RequestParam String processingGroup) {
        configuration.eventProcessingConfiguration()
                     .sequencedDeadLetterProcessor(processingGroup)
                     .map(p -> (MultiTenantDeadLetterProcessor) p)
                     .map(mp -> mp.forTenant(TenantDescriptor.tenantWithId(tenant)))
                     .ifPresent(SequencedDeadLetterProcessor::processAny);
    }
}
Only JPA Dead letter queue and In-Memory queues are supported.

Deadline manager

As of now, there is no plan to support deadline manager out of the box. None of deadline manager implementation support multi-tenancy. See Event scheduler section as alternative.

Event scheduler

You can use the MultiTenantEventScheduler to schedule events for specific tenants. To do so, you can inject the EventScheduler and use it to schedule events:

public class EventHandlingComponentSchedulingEvents {

    private EventScheduler eventScheduler;

    @EventHandler
    public void eventHandler(Event event) {
        // Schedules the given event to be published in 10 days.
        ScheduledToken token = eventScheduler.schedule(Instant.now().plusDays(10), event);
        // The token returned by EventScheduler#schedule can be used to, for example, cancel the scheduled task.
        eventScheduler.cancelSchedule(token);
    }
}

If you use the EventScheduler from any message handling method, it will automatically pick up tenant from Message#metadata. Hence, there is no need to specify the tenant you want to schedule an event for. If you wish to use the EventScheduler outside of message handlers, you are inclined to wrap the execution into a so-called TenantWrappedTransactionManager. Within this TenantWrappedTransactionManager you can schedule the event:

public class EventSchedulingComponent {

    private EventScheduler eventScheduler;

    public void schedule(Event event) {
        ScheduledToken token;
        // Schedules the given event to be published in 10 days.
        new TenantWrappedTransactionManager(
                TenantDescriptor.tenantWithId(tenantName))
                .executeInTransaction(
                        () -> token = eventScheduler.schedule(Instant.now().plusDays(10), event)
                );
        // The token returned by EventScheduler#schedule can be used to, for example, cancel the scheduled task.
        new TenantWrappedTransactionManager(
                TenantDescriptor.tenantWithId(tenantName))
                .executeInTransaction(
                        () -> eventScheduler.cancelSchedule(token)
                );
    }
}

Advanced configuration

Overriding default message source

You can override the default message source for each tenant by defining the following bean:

@Bean
public MultiTenantStreamableMessageSourceProvider multiTenantStreamableMessageSourceProvider(AxonServerEventStore customSource) {
    return (defaultTenantSource, processorName, tenantDescriptor, configuration) -> {
        if (tenantDescriptor.tenantId().startsWith("tenant-custom")) {
            return customSource;
        }
        return defaultTenantSource;
    };
}

This bean should return a StreamableMessageSource that will be used for specific tenants. This lambda will be called for each tenant and each event processor, so be sure to return a default tenant source if you don’t want to override it.

Disable multi-tenancy for specific event processor

In certain cases, you may want to disable multi-tenancy for specific Event Processor which does not have any tenants. For example, when you have an event processor that is consuming events from an external context. Per default, each event processor is scaled, and duplicated for each tenant. To disable this behavior for a specific processing, you can define following bean:

@Bean
public MultiTenantEventProcessorPredicate multiTenantEventProcessorPredicate() {
    return (processorName) -> !processorName.equals("external-context");
}

This bean should return true for each processor that you want to be multi-tenant, and false for each processor that you want to be single tenant.

Tenant Segment Factories

This extension provides several factory interfaces that are used to create tenant-specific segments for various Axon components, such as Command Bus, Query Bus, Event Store, and Event Scheduler. These factories allow you to configure and customize the behavior of these components for each tenant.

The following tenant segment factories are available:

TenantCommandSegmentFactory

This factory is responsible for creating a CommandBus instance for each tenant. By default, it creates an AxonServerCommandBus that uses a SimpleCommandBus as the local segment and connects to Axon Server. You can override this factory to provide a custom implementation of the CommandBus for specific tenants.

TenantQuerySegmentFactory

This factory creates a QueryBus instance for each tenant. By default, it creates an AxonServerQueryBus that uses a SimpleQueryBus as lhe local segment and connects to Axon Server. You can override this factory to provide a custom implementation of the QueryBus for specific tenants.

TenantEventSegmentFactory

This factory is responsible for creating an EventStore instance for each tenant. By default, it creates an AxonServerEventStore that connects to Axon Server. You can override this factory to provide a custom implementation of the EventStore for specific tenants.

TenantEventSchedulerSegmentFactory

This factory creates an EventScheduler instance for each tenant. By default, it creates an AxonServerEventScheduler that connects to Axon Server. You can override this factory to provide a custom implementation of the EventScheduler for specific tenants.

TenantEventProcessorControlSegmentFactory

This factory creates a TenantDescriptor for each event processor, which is used to identify the tenant associated with the event processor. By default, it uses the tenant identifier as the TenantDescriptor. You can override this factory to provide a custom implementation of the TenantDescriptor for specific event processors.