Multi-Tenancy in JPA (Shared vs Separate Schema)

Illustration for Multi-Tenancy in JPA (Shared vs Separate Schema)
By Last updated:

As modern applications scale, supporting multi-tenancy becomes critical. Multi-tenancy means multiple tenants (clients, users, or organizations) share the same application but isolate their data. JPA provides flexible options to achieve this.

There are two primary strategies:

  • Shared Schema (Discriminator Column): All tenants share the same schema, and data is separated using a tenant identifier column.
  • Separate Schema (Schema per Tenant): Each tenant has its own schema or database.

This tutorial explores how to implement multi-tenancy in JPA with examples, performance considerations, and best practices.


1. What is Multi-Tenancy?

Multi-tenancy allows a single application instance to serve multiple tenants while ensuring data isolation and security.

Analogy:

  • Shared Schema = Apartment building with shared walls but separate keys.
  • Separate Schema = Independent houses for each tenant.

2. JPA Multi-Tenancy Strategies

2.1 Shared Schema (Discriminator Column)

  • All tenants share one schema.
  • A tenant_id column distinguishes tenant data.
  • Simple to manage but may affect query performance for large datasets.

2.2 Separate Schema

  • Each tenant has its own schema or database.
  • Provides stronger isolation and better performance at scale.
  • Harder to manage migrations and resource usage.

3. Example Database Setup

Shared Schema

CREATE TABLE customers (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    tenant_id VARCHAR(50),
    name VARCHAR(100),
    email VARCHAR(100)
);

Separate Schema

-- Schema for Tenant A
CREATE SCHEMA tenant_a;
CREATE TABLE tenant_a.customers (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(100)
);

-- Schema for Tenant B
CREATE SCHEMA tenant_b;
CREATE TABLE tenant_b.customers (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(100)
);

4. Shared Schema Implementation in JPA

Entity with Tenant Column

import jakarta.persistence.*;

@Entity
@Table(name = "customers")
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String tenantId; // tenant discriminator

    private String name;
    private String email;
}

Filtering by Tenant

List<Customer> customers = em.createQuery(
    "SELECT c FROM Customer c WHERE c.tenantId = :tenant", Customer.class)
    .setParameter("tenant", "tenant_a")
    .getResultList();

5. Separate Schema Implementation in JPA

Dynamic Schema Switching

With Hibernate, you can set the schema dynamically using MultiTenancyStrategy.

spring.jpa.properties.hibernate.multiTenancy=SCHEMA
spring.jpa.properties.hibernate.multi_tenant_connection_provider=com.example.MultiTenantConnectionProviderImpl
spring.jpa.properties.hibernate.tenant_identifier_resolver=com.example.CurrentTenantIdentifierResolverImpl

Connection Provider Implementation

public class MultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {

    @Override
    public Connection getConnection(String tenantIdentifier) throws SQLException {
        Connection connection = dataSource.getConnection();
        connection.setSchema(tenantIdentifier); // switch schema dynamically
        return connection;
    }
}

Tenant Identifier Resolver

public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {

    @Override
    public String resolveCurrentTenantIdentifier() {
        return TenantContext.getCurrentTenant(); // thread-local tenant
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }
}

6. Real-World Integration with Spring Boot

Spring Boot + Hibernate makes tenant resolution seamless.

@Service
public class CustomerService {

    @PersistenceContext
    private EntityManager em;

    @Transactional
    public void createCustomer(String tenantId, String name, String email) {
        TenantContext.setCurrentTenant(tenantId);
        Customer c = new Customer();
        c.setTenantId(tenantId);
        c.setName(name);
        c.setEmail(email);
        em.persist(c);
    }
}

7. Performance Considerations

  • Shared Schema: Easier to maintain, but indexing tenant_id is crucial for performance.
  • Separate Schema: Scales better but increases operational complexity.
  • For both, caching strategies and connection pooling must be optimized.

8. Pitfalls and Anti-Patterns

  • Mixing Tenants: Ensure tenant filters are always applied to avoid data leaks.
  • Complex Migrations: Separate schema migrations can become costly.
  • Excessive Eager Fetching: May degrade performance with large tenant datasets.
  • Overusing Cascade: Avoid heavy cascades across tenant boundaries.

9. Best Practices

  • Use shared schema for small/medium applications.
  • Use separate schema for enterprise-scale apps requiring stronger isolation.
  • Always enforce tenant filters in repositories or services.
  • Implement automated migrations for schema-per-tenant setups.
  • Monitor SQL queries to catch cross-tenant data access.

📌 JPA Version Notes

  • JPA 2.0: Core ORM features, no built-in multi-tenancy support.
  • JPA 2.1: Enhanced schema generation, useful for multi-tenancy setups.
  • Hibernate Extensions: Added full multi-tenancy strategies (DATABASE, SCHEMA, DISCRIMINATOR).
  • Jakarta Persistence (EE 9/10/11): Package rename (javax.persistencejakarta.persistence). No major multi-tenancy changes, but better cloud-native support.

Conclusion and Key Takeaways

  • Multi-tenancy enables serving multiple clients in a single JPA application.
  • Shared Schema = simpler, but less isolated.
  • Separate Schema = more isolated, but harder to manage.
  • Choose based on application scale, performance, and compliance needs.

FAQ (Expert-Level)

Q1: What’s the difference between JPA and Hibernate?
A: JPA is a specification, Hibernate is a provider that adds extended features like multi-tenancy.

Q2: How does JPA handle the persistence context?
A: JPA tracks managed entities in the persistence context, ensuring tenant isolation with filters or schema switching.

Q3: What are the drawbacks of eager fetching in JPA?
A: It loads unnecessary tenant data, hurting performance.

Q4: How can I solve the N+1 select problem with JPA?
A: Use fetch joins, entity graphs, or batch fetching.

Q5: Can I use JPA without Hibernate?
A: Yes, but Hibernate provides the richest multi-tenancy support.

Q6: Which multi-tenancy strategy is best for SaaS?
A: Shared schema for startups, separate schema for enterprise SaaS requiring strict isolation.

Q7: How does JPA handle composite keys?
A: Using @IdClass or @EmbeddedId annotations.

Q8: What changes with Jakarta Persistence?
A: Mainly package renaming and better cloud-native runtime alignment.

Q9: Is JPA suitable for microservices?
A: Yes, but tenant context resolution must be lightweight.

Q10: When should I avoid JPA multi-tenancy?
A: Avoid it in apps with few tenants where a single schema is simpler to manage.