๐๐๏ธ DATABASE MULTITENANCY with Spring Boot & ARCONIA
๐๐๏ธ DATABASE MULTITENANCY with Spring Boot & ARCONIA
One SaaS. Many customers. How far should data isolation go?
๐ธ TL;DR
A multitenant SaaS can store every customer's data in the same tables, separate schemas, or separate databases. Arconia brings the tenant context + database routing needed to implement the database-per-tenant model cleanly with Spring Boot.

๐ธ 1๏ธโฃ WHAT IS ARCONIA?
Arconia is an open-source add-on framework for Spring Boot focused on modern enterprise applications.
It provides modules for:
โช๏ธ Multitenancy
โช๏ธ Dev Services
โช๏ธ OpenTelemetry
โช๏ธ AI observability
โช๏ธ Cloud-native tooling
It complements Spring Boot rather than replacing it.
For database multitenancy, two pieces are particularly interesting:
HTTP request
โ
Arconia TenantContext
โ
TenantDataSource
โ
Customer databaseArconia resolves the current tenant, propagates it through the request, then its Data JDBC module can route JDBC connections to the corresponding database.
๐ธ 2๏ธโฃ WHAT IS MULTITENANCY?
A tenant is usually a customer organization, not an individual user.
Imagine a CRM SaaS:
CRM SaaS
โโโ ACME
โ โโโ Alice
โ โโโ Bob
โ
โโโ GLOBEX
โโโ John
โโโ SarahEveryone runs on the same SaaS product, but:
ACME must never see GLOBEX's CRM data.
The architectural question becomes:
Where do we create that isolation boundary?
๐ธ 3๏ธโฃ THE 3๏ธโฃ DATABASE MULTITENANCY STRATEGIES
There are three classic approaches.
1๏ธโฃ SHARED DATABASE + SHARED TABLES
customers -------------------------------- id | tenant_id | customer_name 1 | ACME | Foo Corp 2 | GLOBEX | Bar Corp
Every tenant shares the same tables.
Isolation comes from something like:
SELECT *
FROM customers
WHERE tenant_id = 'ACME';โ Cheapest and easiest to operate
โ ๏ธ A missing tenant filter can expose another customer's data
-
2๏ธโฃ SHARED DATABASE + SEPARATE SCHEMAS
crm_db โโโ acme.customers โโโ globex.customers
Same database server, but each tenant gets its own schema.
โ Stronger logical isolation
โ Tables don't require tenant_id everywhere
โ ๏ธ More schemas and migrations to manage
-
3๏ธโฃ DATABASE PER TENANT
ACME โ crm_acme GLOBEX โ crm_globex FOO โ crm_foo
Each customer gets its own database.
โ Strongest of these three data-isolation boundaries
โ ๏ธ More databases, pools, migrations and operations to manage
Hibernate also describes these as discriminator-, schema-, and database-based multitenancy.
๐ธ 4๏ธโฃ WHY DATABASE-PER-TENANT?
Why not simply add tenant_id everywhere?
Because sometimes strong isolation is worth the operational cost.
โช๏ธ Data isolation A query against ACME's DB cannot accidentally return GLOBEX rows.
โช๏ธ Smaller blast radius A bad query such as:
DELETE FROM customers;
damages one tenant database rather than every tenant stored in the same tables.
โช๏ธ Independent backup & restore Restore ACME without restoring the complete SaaS dataset.
โช๏ธ Independent scaling A huge customer can move to stronger infrastructure.
โช๏ธ Data residency European and Asian customers can potentially live in different infrastructure locations.
โช๏ธ Lifecycle management Onboarding, migration or deletion can happen database by database.
But database-per-tenant isn't free: Arconia notes that dynamically created tenant data sources each hold a connection pool, so the number of tenant data sources must be controlled.
And separate databases don't automatically mean separate physical infrastructure: if several databases share the same DB server, a server-level failure can still affect them all.
๐ธ 5๏ธโฃ USE CASE: A MULTITENANT CRM SaaS
Imagine:
CRM SaaS
โ
Spring Boot API
โ
โโโโโโโโโดโโโโโโโโ
โ โ
ACME GLOBEX
โ โ
crm_acme DB crm_globex DBAlice authenticates as an ACME user.
Her request carries:
tenant = ACME
The application determines the tenant once:
Request โ Tenant = ACME โ TenantContext โ TenantDataSource โ crm_acme
Your business code can still simply ask:
customerRepository.findAll();
It doesn't need:
findAllByTenantId("ACME");The database connection itself is already pointing at ACME's database.
That's the interesting part.
๐ธ 6๏ธโฃ SET IT UP WITH ARCONIA IN 5๏ธโฃ STEPS
Arconia Multitenancy currently requires Java 25+, because its tenant context uses Java ScopedValue.
1๏ธโฃ ADD ARCONIA MULTITENANCY
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.arconia</groupId>
<artifactId>arconia-bom</artifactId>
<version>0.30.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.arconia</groupId>
<artifactId>arconia-multitenancy-web-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.arconia</groupId>
<artifactId>arconia-multitenancy-data-jdbc</artifactId>
</dependency>
</dependencies>The Web starter resolves and propagates the tenant. arconia-multitenancy-data-jdbc provides TenantDataSource, which implements database-per-tenant JDBC routing. The Data JDBC module intentionally has no auto-configuration.
-
2๏ธโฃ DECLARE YOUR CRM TENANTS
arconia:
multitenancy:
details:
tenants:
- identifier: acme
enabled: true
- identifier: globex
enabled: true
Arconia can maintain a known set of tenants and reject unknown or disabled identifiers. For dynamic SaaS onboarding, tenant details can instead come from JDBC or your own TenantDetailsService.
-
3๏ธโฃ CONFIGURE THE CUSTOMER DATABASES
acme: url: jdbc:postgresql://db-acme/crm username: ${ACME_DB_USER} password: ${ACME_DB_PASSWORD} globex: url: jdbc:postgresql://db-globex/crm username: ${GLOBEX_DB_USER} password: ${GLOBEX_DB_PASSWORD}
Each CRM tenant maps to connection details controlled by the application. Don't construct JDBC URLs directly from an untrusted tenant identifier: use it as a validated lookup key.
-
4๏ธโฃ ROUTE CONNECTIONS WITH TenantDataSource
@Bean
TenantDataSource tenantDataSource(
DataSource acmeDataSource,
DataSource globexDataSource) {
return TenantDataSource.builder()
.dataSource("acme", acmeDataSource)
.dataSource("globex", globexDataSource)
.build();
}TenantDataSource checks the current tenant and delegates JDBC connections to its database. It works with plain JDBC, JdbcClient, JdbcTemplate, and Spring Data JDBC.
-
5๏ธโฃ LET ARCONIA RESOLVE THE TENANT
@GetMapping("/customers") List<Customer> customers( @TenantIdentifier String tenant) { return customerRepository.findAll(); }
curl \ -H "X-TenantId: acme" \ http://localhost:8080/customers https://docs.arconia.io/arconia/latest/multitenancy/web/
By default, the Web starter resolves X-TenantId, validates it and binds it to TenantContext. JDBC access then reaches ACME's datasource rather than requiring tenant_id in each repository query.
The resulting architecture is:
X-TenantId: acme
โ
โผ
TenantContext = acme
โ
โผ
TenantDataSource
โ
โผ
crm_acme
โ
โผ
customers๐ธ TAKEAWAYS
โช๏ธ Multitenancy does not automatically mean one DB per customer.
โช๏ธ Shared tables + tenant_id are often the simplest and cheapest model.
โช๏ธ Separate schemas provide an intermediate isolation level.
โช๏ธ Database-per-tenant increases isolation and reduces the potential blast radius of tenant-specific data failures.
โช๏ธ The price is operational complexity: more databases, migrations and connection pools.
โช๏ธ Arconia makes the database-per-tenant model interesting because tenant resolution and JDBC routing become infrastructure concerns instead of business-code concerns. (findAll() and not findAllById("Acme"))
Your repository can remain:
customerRepository.findAll();
while the architecture decides whether that means:
ACME โ crm_acme
or:
GLOBEX โ crm_globex
Same Spring Boot application. Different tenant. Different database. ๐๏ธ
#Java #Spring #SpringBoot #Arconia #Multitenancy #SaaS #PostgreSQL #JDBC #SoftwareArchitecture #CloudNative
See Spring Dev Advocate video talking of that topic: https://youtu.be/uZepsaaASO4?si=_VzzOeYH6JYvvwzY
Go further with Java certification:
Java๐
Spring๐
SpringBook๐
JavaFullstackBook๐
