tenantlayer.io

Isolation strategies

Discriminator column

Every tenant's rows share a table, separated by a column. The strategy most SaaS starts with, and the one to start with unless you have a reason not to.

@Entity
class Note {
    @Id @GeneratedValue Long id;
 
    @TenantId
    private String tenantId;
 
    private String body;
}

Hibernate adds the predicate to reads and stamps the column on writes. The application never mentions the tenant. TenantLayer supplies the CurrentTenantIdentifierResolver that makes it work, wired automatically (tenantlayer.discriminator.enabled, on by default).

When no tenant is bound, the resolver returns the empty string. Reads then match nothing and writes stamp a value no tenant uses, so work attempted without a tenant does nothing rather than touching everyone's rows. Returning "default" or the first known tenant would be the opposite of that.

Row-level security

The same shared table, with Postgres applying the predicate instead of Hibernate. See Row-level security.

Use both

They are not alternatives, and the difference is what happens when someone steps outside the ORM.

DiscriminatorRLS
JPA queriesfilteredfiltered
Native SQL, JdbcTemplatenot filteredfiltered
Bulk update/deletepartlyfiltered
A psql session on the same credentialsnot filteredfiltered
Requires Postgresnoyes

The discriminator gives you a tenant column that is populated correctly without any code remembering to do it. RLS gives you the guarantee that holds when code does something the ORM never sees. Enabling both costs one annotation and one policy.

Note what neither covers: a connection that has been unwrapped to a raw PgConnection, and anything running as superuser. The first is why enforcement at the JDBC layer is a Pro concern (Tenant Guard); the second is why your application must not connect as one.

Schema-per-tenant and database-per-tenant

v0.2 and v0.3 respectively. The registry already carries datasource_ref so that adopting them later is a routing change rather than a migration.