Return to site

🍃🗄️ DATABASE MULTITENANCY with Spring Boot & ARCONIA

One SaaS. Many customers. How far should data isolation go?

· spring

🔸 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.

Section image

🔸 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 database

Arconia 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
      └── Sarah

Everyone 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.

Section image

🔸 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 DB

Alice 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👇