Sample Spring Boot applications with Different multi tenancy approaches.
Multi-tenancy is a software architecture in which a single instance of a software provides a service for multiple customers or "tenants." A noteworthy example of multi-tenancy software is Software as a Service (SaaS). One server shared by many users, each with its own data.
One of the main reasons is multi-tenancy allows us to scale and serve a large number of clients using shared computing resources. Another reason is that tenants are served from the same application stack. This simplifies the overall deployment and operational complexity, and reduces cost associated with operations.
Multi-tenant applications which require tenant data isolation in the database layer. There are different approaches for achieving this data isolation. The most widely used approaches are
- Shared schema: Tenants share common schema and are distinguished by a tenant discriminator column.
- Separate schemas: Tenants share common database but each tenant has its own set of tables (schema).
- Separate-databases: Each tenant has its own database.
Shared schema approach is the easiest and most cost-effective. Here, all the tenants share the same database, the same schema and even the same tables. As a result, data records for all the tenants are stored together in these common tables. Each of these common tables have an indexed tenant ID column which is used to retrieve records belonging to a specific tenant.
Separate schema per tenant approach, introduces physical separation between tenants while still using the same database, but creating a new schema for every tenant. All the tables belonging to the tenant then reside within that schema and the data for the tenant is somewhat isolated from other tenant data
Each tenant's data is kept in a physically separate database instance. This is the highest level of isolation. It adds further isolation between the tenants by creating a brand new database for every tenant. Each of these databases have the same schema and migrations are run on all the tenant databases during deployment.
Hibernate officially supports two different multi-tenancy mechanisms:
- separate schema: every tenant has its own schema in a shared database
- separate database: every tenant has its own database instance
Unfortunately, both of these mechanisms come with some downsides in terms of scaling. A third Hibernate multi-tenancy mechanism, a tenant discriminator, also exists, and it’s usable—but it’s still considered a work-in-progress by some.
There is a open issue HHH-6054 to Support for discriminator-based multi-tenancy in hibernate.
Hibernate provides filters which allow for parameterized data to be used in a conditional to determine if queried data should be returned or not. With this we can implement discriminator-based multi-tenant application.
In this repo I have included Implementation details for all three approaches
- Java 1.8
- Gradle
- Spring boot 2
- Hibernate
- Postgres 9.6 or greater
- Flyway
In this Sample we have taken a simple ContactList Management Implementation.
- A Tenant can create One or more ContactList in the System
- A Tenant can create One or more Contact to a particular List
All apis Should have custom header X-Tenant-ID to identify the tenant.
HTTP Method | Endpoint | Response | HTTP Status |
---|---|---|---|
GET | /api/lists | Returns all lists of the system in JSON. | 200/204/401/500 |
POST | /api/lists | Create a user in the system. | 200/204/401/500 |
GET | /api/lists/{listId} | Returns a user in JSON. | 200/204/401/500 |
PUT | /api/lists/{listId} | Returns a user in JSON. | 201/409/500 |
DELETE | /api/lists/{listId} | Delete a list by Id. | 200/401/409/500 |
GET | /api/lists/{listId}/contacts | Returns all contacts of a lists in JSON. | 200/204/401/500 |
POST | /api/lists/{listId}/contacts | Create a contact in the list. | 200/204/401/500 |
GET | /api/lists/{listId}/contacts/{id} | Returns a contact from the list in JSON. | 200/204/401/500 |
PUT | /api/lists/{listId}/contacts/{id} | Update a contact in the list. | 201/409/500 |
DELETE | /api/lists/{listId}/contacts/{id} | Delete a contact by Id and listId | 200/401/409/500 |
We need one place, where tenant information will be stored. It should be available across threads, service classes, simply anywhere, where tenant specific code could be executed.
public abstract class TenantContextHolder {
private static ThreadLocal<TenantContext> tenants;
private static ThreadLocal<TenantContext> inheritableTenants;
static {
tenants = new NamedThreadLocal<>("tenants Context");
inheritableTenants = new NamedInheritableThreadLocal<>("tenants Context");
}
public static void reset() {
tenants.remove();
inheritableTenants.remove();
}
public static TenantContext getContext() {
TenantContext context = tenants.get();
if (context == null) {
context = inheritableTenants.get();
}
return context;
}
public static void setTenant(Tenant tenant) {
TenantContext tenantContext = new TenantContext(tenant);
setTenant(tenantContext);
}
public static void setTenant(Tenant tenant, boolean inheritable) {
TenantContext tenantContext = new TenantContext(tenant);
setTenant(tenantContext, inheritable);
}
public static void setTenant(TenantContext tenantContext) {
setTenant(tenantContext, false);
}
public static void setTenant(TenantContext tenantContext, boolean inheritable) {
if (tenantContext == null) {
reset();
} else {
if (inheritable) {
inheritableTenants.set(tenantContext);
tenants.remove();
} else {
tenants.set(tenantContext);
inheritableTenants.remove();
}
}
}
}
The TenantContextHolder will hold the tenant information across the threads for each request. At the end of each request this will be clear out.
An interceptor that reads the tenant identifier from the request header (or JWT in different implementations) and sets the tenant context.
@Component
public class TenantInterceptor extends HandlerInterceptorAdapter {
public static final String TENANT_HEADER = "X-TenantID";
Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
TenantRepository repository;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String tenantHeader = request.getHeader(TENANT_HEADER);
boolean tenantSet = false;
if (!tenantHeader.isEmpty()) {
Tenant tenant = repository.findByName(tenantHeader).orElseThrow(() -> new RuntimeException("Invalid Tenant :" + tenantHeader));
logger.debug("Set TenantContextHolder: {}", tenant);
TenantContextHolder.setTenant(tenant, true);
tenantSet = true;
} else {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write("{\"error\": \"No tenant supplied\"}");
response.getWriter().flush();
}
return tenantSet;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
logger.debug("Clear TenantContextHolder: {}", TenantContextHolder.getContext());
TenantContextHolder.reset();
}
}