🍃🗄️ 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👇
