Tenant Details
Arconia Multitenancy validates the syntax of every tenant identifier that enters the application, and provides an optional mechanism for managing tenant metadata and checking identifiers against a known set of tenants.
Two distinct checks are involved, and they are configured independently:
-
Identifier validation inspects the identifier itself and is always applied. See Tenant Identifier Validation.
-
Tenant verification loads the tenant and checks that it exists and is enabled. It requires a
TenantDetailsService. See Tenant Verification.
TenantDetails
The TenantDetails interface represents the core information about a tenant:
public interface TenantDetails {
String identifier();
boolean enabled();
default Map<String, Object> attributes() {
return Map.of();
}
}
Arconia provides a default implementation (Tenant record) that supports all these properties and can be constructed using a builder pattern:
var tenant = Tenant.builder()
.identifier("acme")
.enabled(true)
.addAttribute("plan", "premium")
.build();
TenantDetailsService
The TenantDetailsService interface defines the contract for loading tenant information:
public interface TenantDetailsService {
List<? extends TenantDetails> loadAllTenants();
TenantDetails loadTenantByIdentifier(String identifier);
}
When a TenantDetailsService bean is available, a DefaultTenantVerifier is auto-configured that checks each resolved tenant identifier against the service. See Tenant Verification for details. Identifier syntax is validated separately, whether or not a TenantDetailsService is configured.
Properties-Based Tenant Details
For simple use cases, you can define tenants directly in your application configuration:
arconia:
multitenancy:
details:
tenants:
- identifier: acme
enabled: true
attributes:
plan: premium
region: eu-north-1
- identifier: beans
enabled: true
- identifier: pixie
enabled: false
attributes:
onboarding: true
As soon as at least one tenant is configured, Arconia auto-configures a PropertiesTenantDetailsService that loads tenant details from the configuration. Requests with a tenant identifier not in the list or for a disabled tenant will be rejected.
| Property | Default | Description |
|---|---|---|
|
|
List of tenant configurations. |
|
Unique identifier for the tenant. Required. |
|
|
|
Whether the tenant is enabled. |
|
|
Additional metadata for the tenant as key-value pairs. |
JDBC-Based Tenant Details
For dynamic tenant management, tenant details can be stored in a relational database and loaded via JDBC. Add the Arconia Multitenancy Tenant Details JDBC Spring Boot Starter dependency to your project.
-
Gradle
-
Maven
dependencies {
implementation 'io.arconia:arconia-multitenancy-details-jdbc-spring-boot-starter'
}
<dependency>
<groupId>io.arconia</groupId>
<artifactId>arconia-multitenancy-details-jdbc-spring-boot-starter</artifactId>
</dependency>
The dependency is all you need: Arconia auto-configures a JdbcTenantDetailsService that loads tenant details from the DataSource available in the application. No further configuration is required.
If tenants are also declared via arconia.multitenancy.details.tenants, the JDBC implementation takes precedence. Set arconia.multitenancy.details.jdbc.enabled to false to use the configured list instead, without removing the dependency.
Tenants are stored in a tenant_details table and their attributes in a tenant_details_attributes table.
For example, this is how the tables are created in a PostgreSQL database:
create table tenant_details
(
id serial primary key,
identifier text default pg_catalog.gen_random_uuid() not null unique,
enabled boolean default true not null,
created_at timestamp default now() not null
);
create table tenant_details_attributes
(
tenant_id integer not null references tenant_details (id),
attribute_name text not null,
attribute_value text not null,
primary key (tenant_id, attribute_name)
);
Schema scripts are bundled for PostgreSQL and H2, together with matching scripts to drop the tables again.
|
The bundled scripts are applied to embedded databases only, following the same convention as the Spring Boot SQL initialization. On PostgreSQL, or any other database that is not embedded, the tables are not created for you. The application still starts, and the first request that resolves a tenant then fails with a message like Either let Arconia create them:
or create them yourself, with the bundled script as a starting point, and set |
For a database without a bundled script, point schema to your own script. The @@platform@@ placeholder in the location is replaced with the identifier of the database driver in use.
| Property | Default | Description |
|---|---|---|
|
|
Whether tenant details are loaded from a relational database. |
|
|
Database schema initialization mode. Options: |
|
|
Path to the SQL file to use to initialize the database schema. |
|
Platform to use in the schema script if the |
|
|
|
Whether initialization should continue when an error occurs when applying the schema script. |
Custom TenantDetailsService
For dynamic tenant management from another source, implement the TenantDetailsService interface and register it as a Spring bean:
import io.arconia.multitenancy.core.tenantdetails.TenantDetails;
import io.arconia.multitenancy.core.tenantdetails.TenantDetailsService;
@Bean
TenantDetailsService tenantDetailsService(TenantRepository tenantRepository) {
return new TenantDetailsService() {
@Override
public List<? extends TenantDetails> loadAllTenants() {
return tenantRepository.findAll();
}
@Override
public TenantDetails loadTenantByIdentifier(String identifier) {
return tenantRepository.findByIdentifier(identifier);
}
};
}
When a custom TenantDetailsService is registered, it takes precedence over the built-in implementations, and tenant verification is automatically enabled.
Tenant Identifier Validation
Before a tenant is looked up, the syntax of the resolved identifier is validated. The identifier arrives from an untrusted source such as an HTTP header or a subdomain, and it goes on to select tenant-specific resources, so it is checked at the boundary regardless of how tenant details are configured.
The TenantIdentifierValidator interface defines the contract:
public interface TenantIdentifierValidator {
void validate(String tenantIdentifier);
}
Arconia always auto-configures a DefaultTenantIdentifierValidator. It accepts identifiers made up of alphanumeric characters, dashes, and underscores, up to 64 characters. Anything else is rejected with a TenantVerificationException, resulting in an HTTP 400 Bad Request response.
To accept longer identifiers, register the default implementation with a different limit:
import io.arconia.multitenancy.core.tenantdetails.DefaultTenantIdentifierValidator;
import io.arconia.multitenancy.core.tenantdetails.TenantIdentifierValidator;
@Bean
TenantIdentifierValidator tenantIdentifierValidator() {
return DefaultTenantIdentifierValidator.builder()
.maxLength(128)
.build();
}
For a different set of accepted characters, provide your own implementation. It takes precedence over the auto-configured default:
import io.arconia.multitenancy.core.exceptions.TenantVerificationException;
import io.arconia.multitenancy.core.tenantdetails.TenantIdentifierValidator;
@Bean
TenantIdentifierValidator tenantIdentifierValidator() {
return tenantIdentifier -> {
if (!tenantIdentifier.matches("[a-z0-9]{8}")) {
throw new TenantVerificationException("The tenant identifier must be 8 lowercase alphanumeric characters");
}
};
}
Keep a custom validator strict. A tenant identifier that passes validation can be used to select a database, a schema, or a cache partition, and it is recorded in logs and observations.
Tenant Verification
The TenantVerifier interface defines the contract for verifying that a resolved tenant identifier belongs to a tenant that exists and is allowed to proceed. When a TenantDetailsService is available, Arconia auto-configures a DefaultTenantVerifier that checks the tenant exists and is enabled. Unlike identifier validation, verification is only applied when a TenantDetailsService is configured.
Entry points such as the TenantContextFilter call the verifier directly before establishing the tenant context. If verification fails, a TenantVerificationException is thrown, resulting in an HTTP 400 Bad Request response.
You can provide a custom TenantVerifier bean to implement your own verification logic:
import io.arconia.multitenancy.core.tenantdetails.TenantVerifier;
@Bean
TenantVerifier tenantVerifier() {
return tenantIdentifier -> {
// Custom verification logic (e.g., check quotas, feature flags)
};
}
Actuator
Tenants Endpoint
When Spring Boot Actuator is present in the classpath and a TenantDetailsService is available, Arconia auto-configures a tenants actuator endpoint. It exposes the tenants known to the application, so you can check at runtime which tenants are registered and which of them are enabled.
By default, Spring Boot exposes only the health endpoint over HTTP. To access the tenants endpoint, include it in the web exposure configuration:
management:
endpoints:
web:
exposure:
include: tenants
| Property | Default | Description |
|---|---|---|
|
|
Permitted level of access for the tenants endpoint. Options: |
|
|
Maximum time that a response can be cached. |
List All Tenants
http :8080/actuator/tenants
Response example:
{
"tenants": [
{
"identifier": "acme",
"enabled": true
},
{
"identifier": "beans",
"enabled": true
},
{
"identifier": "pixie",
"enabled": false
}
]
}
Show a Single Tenant
http :8080/actuator/tenants/acme
Response example:
{
"identifier": "acme",
"enabled": true,
"attributeNames": [
"plan",
"region"
]
}
An unknown tenant identifier returns HTTP 404 Not Found.
|
The endpoint reports attribute names but never attribute values. Tenant attributes are defined by the application and may carry credentials or connection details, which should not be readable from an actuator endpoint. Use the |
Health Indicator
When tenant details are loaded from a relational database and Spring Boot Actuator is present in the classpath, Arconia auto-configures a health indicator for the database holding them. It reports the application as down when that database cannot be reached, which is the condition that would otherwise make every request fail tenant verification.
The check is a bounded count against the tenant details table rather than a full read, so polling it does not scan every tenant. The number of registered tenants is reported as a detail.
{
"status": "UP",
"components": {
"tenantdetails": {
"status": "UP",
"details": {
"tenants": 3
}
}
}
}
| Property | Default | Description |
|---|---|---|
|
|
Whether the tenant details health indicator should be enabled. |
The health indicator is only auto-configured for the JDBC-based tenant details. A properties-based or custom TenantDetailsService has no external dependency to check, so none is registered.