Multitenancy architecture with discriminator column — Part 1

Using Java, Spring Web, Spring Data JPA (Hibernate) and Spring Security

Filipe Martins
7 min readFeb 14, 2020

Versão em português do Brasil: https://link.medium.com/P5ZWbWtf43

Good morning, good afternoon or good night!

This is my first article, so I will introduce myself: My name is Filipe Martins, husband, father and senior software analyst at Logicalis Brazil.

Introduction

In one of my personal projects I was in need of a multitenancy architecture and that is what moved everything did and present it to you in this article. Let’s explain:

What is multitenancy?

The term multitenancy, in general, is applied to software development to indicate an architecture in which a single running instance of an application simultaneously serves multiple tenants (customers). Isolating information (data, customizations, etc.) belonging to the various tenants (customers) is a particular challenge in these systems. This includes the data pertaining to each tenant (customer) stored in the database.

How many ways to do multitenancy are there? The most common are:

Separate databases:

The data for each tenant (customer) is kept in separate database instances.

Separate schemes:

The data for each tenant (client) is kept in separate schemas, however the same database instance is used.

Data partitioned through a discriminating column:

The data of each tenant (client) is kept in a single schema of a single instance of the database and they are partitioned using a column that indicates which tenant (client) owns that record.

There are pros and cons to each approach that are outside the scope of this article.

Which use case did I need?

Well, the title of the article is already spoiled, but for my use case i needed the third approach, but Hibernate doesn’t have native support yet, as you can see in the documentation.

DISCRIMINATOR
Correlates to the partitioned (discriminator) approach. It is an error to attempt to open a session without a tenant identifier using this strategy. This strategy is not yet implemented and you can follow its progress via the HHH-6054 Jira issue.

And with that started my search to find implementations, articles and examples. In those I found, when I went to test, it was possible for a tenant (customer) to see data from another! Serious failure!

Well, that’s where the project that I made available on GitHub was born. The reason for sharing it and writing this article are:

  • Share knowledge.
  • Put my implementation to the test in any ways: Security, scalability, readability and etc. and, with that, if there is something to improve, improve, simple as that.

Project repository

Running the project “AS-IS”

For this you will need:

  • Java SE Development Kit 8
  • PostgresSQL 9+
  • Some development IDE that allows you to use the Lombok plugin.
    I recommend the excellent IntelliJ IDEA.
  • For testing APIs I recommend using Postman.

The easiest way to run PostgreSQL is using Docker. With it installed, just run the command docker run --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres

Now just run the DemoApplication class. In the “IntelliJ IDEA” right click on it and click on Run 'DemoApplication.main()'.

Main points of the project development

If you got here, you didn’t just want to get the code ready and use it, so let’s go through the main points together.

Tenantable is a class whose mapping information is applied to the entities that inherit from it, therefore, it does not have a table in the database for it. We do this by annotating the class with @MappedSuperclass.

  • Lines 3 to 6: This is responsible for inserting filters in the queries (selects) of all classes that inherit from it. We will see later on how these filters will be populated automatically.
  • Line 7: Class responsible for “listening” to operations performed in all classes that inherit from it. Let’s see the implementation below.
  • Lines 6 to 10: Before an insert (@PrePersist) or update (@PreUpdate) the prePersistOrUpdate method will be executed. If the object is an instance of the Tenantable class, the tenantId attribute will have the tenantId of the tenantContext as value.
    Resume: Here we are ensuring that data inserted or updated is in the tenant of the authenticated user.
  • Lines 14 to 17: Before a delete (@PreRemove) the preRemove method will be executed and if the object is an instance of the Tenantable class AND the tenantId attribute of the object trying to be deleted is different from the tenantId attribute from the tenantContext, an EntityNotFoundException error will be throw. Otherwise, it is successfully deleted.
    Resume: Here we are protecting ourselves from attempts by a tenant user to erase data from another tenant.

We mentioned the TenantContext class above. Let’s see what the purpose of it below.

Simple isn’t it? It is responsible for encapsulating the way we retrieve the tenantId of the authenticated user.

The same is obtained from the Authentication object of SecurityContextHolder, that is, added through a Spring Security filter. Let’s see how this is done below.

The above class is a Spring Security filter. There are several available depending on the use case, but for this article we will use the one that simulates an authenticated user with the email foo@bar.com from tenant 1.

TIP: This is where we can put the logic of obtaining user credentials, especially one that is widely used in REST APIs, the JWT, but that is outside the scope of this article … but who knows in the next one, right? ; )

The class above is the one who enables and allows configuring the security provided by Spring Security.

  • Line 10: The customized filter we created is placed in the security filter chain.

TIP: This is where we can define authorization rules for accessing routes from our APIs, using antMatchers, but that’s for the next article or just search and DYI! ; )

Now one of the most interesting classes. It uses Spring AOP, which you can read in more detail at: https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop

  • Line 11: When any method that starts with find, with any number of arguments, from a repository that inherits from TenantableRepository is executed, then the beforeFindOfTenantableRepository method will be executed.
  • Lines 13 to 16: We take the persistence session, enable the filter we saw in the first class (Tenantable) and fill it with the value of the authenticated user’s tenantId.

Resume: When a SELECT is made on a repository that inherits from TenantableRepository, the “WHERE tenant_id =?” will be inserted in the SELECT and the ? will be replaced by the authenticated user’s tenant ID.

In line 11, we have a reference to the class TenantableRepository. We will see below an extra reason for its existence besides allowing what we have seen so far.

This is the class that every repository needs to inherit if it wants to deal with multi tenant entities. Finally, let’s look at the UserService class, whose UserRepository inherits from the class above.

Almost all methods in this class speak for themselves, so we will only talk about updateById. The reason we need to talk about this specific point is that someone may ask:

Hey! Why didn’t you use the findById method that is already implemented?

It would be great, but it wasn’t possible because of this: https://stackoverflow.com/questions/45169783/hibernate-filter-is-not-applied-for-findone-crud-operation

Explaining: The findById implementation does not do an entity query, but a direct search, so the filters that we want to be filled in automatically, in this case, are not.
Resume: When searching for the entity with findById it will not make a WHERE for tenantId in SELECT, thus being able to get a record from another tenant to UPDATE. And that can’t happen!

Going back to the UserService class.

  • Line 16: We use the findOneById method, which is now an entity query, that starts with find and that our TenantAspect class will be able to intercept and fill the tenantId filter. If the user is trying to update another tenant’s records, he will simply receive an EntityNotFoundException.

Conclusion

We saw the main points of the project that allows to have a multitenancy application using a discriminator column, which in most cases, and in mine, is called tenant_id.

Testing

Do you remember the TenantAuthorizationFilter class?
It is in it that we simulate that the user foo@bar.com of tenant 1 is authenticated in the system, then:

  1. Create as many tenants as you want.
  2. List them and get any id.
  3. Place the chosen id in the second parameter, tenantId, of the constructor of the class TenantAuthenticationToken.
  4. Restart the application to simulate authentication with the tenant chosen in step 2.
  5. Create users.
  6. Repeat step 2, get another id and repeat steps 3 and 4.
  7. Now try to list, update or delete a user from another tenant.
  8. Do these and other tests as you wish.

List of available APIs and body examples (JSON)

Tenants

POST http://localhost:8080/api/tenants
Body example: {“name”: “Tenant 1”}

GET: http://localhost:8080/api/tenants

Users

POST http://localhost:8080/api/users
Body example: {“name”: “User 1”}

GET http://localhost:8080/api/users

PATCH http://localhost:8080/api/users/1
Body example: {“name”: “User 1 v2”}

DELETE: http://localhost:8080/api/users/1

Project bonus

I implemented features, listed below, that are not part of the scope of this article and that would make it bigger than it already is.

  • JPA Auditing: Makes all entities that inherit from Auditable have the createdBy and createdDate fields populated when inserting an entity and lastModifiedBy and lastModifiedDate populated when updating an entity, automatically.

In the “application.properties” file

  • Lines 1 to 3: Setting up the database connection through environment variables.
    Line 5: Setting up Allowed Origins of CORS through environment variable.

Well, that’s it folks! I hope you enjoyed! The second and last part of the article is available here.

--

--