Elegant Multi-Tenancy for Microservices — Part II: Solutioning
An End-To-End Solution Your Team Will Love by Jakob Rodseth
Solutioning
Based on the design decisions in part I, we’ll start in the data access layer (DAL) and assume we have access to any data external to our imaginary inventory microservice we need in order to inform our data interactions. We’ll solution creating, reading, updating, and deleting database objects in a tenant-aware manner using our assumed data, then add supporting infrastructure to provide us with the data.
Reading from the DB
Hibernate is a popular ORM that implements the JPA spec which Spring Boot supports through the Spring Data JPA module. Leveraging its features provides a fast route to a solution.¹ Hibernate added multi-tenancy support via both separate database and schema and was supposed to support discriminator field multi-tenancy in release 5.0, but the feature is yet to be implemented as of the time of writing. We’ll have to roll our own solution instead.
Hibernate provides filters which allow for parameterized data to be used in a conditional to determine if queried data should be returned or not.
Creating the Hibernate Filter:
The filter is set up via
- I.)
@FilterDef
which defines a filter called“tenantFilter”
with a - II.)
“tenantId”
parameter of typeLong
and a - III.) default condition that the parameter
“tenantId”
must match a property on the object called IV.)“tenantId”
. - V.) We then apply the filter to the Inventory class using the
@Filter
annotation.
Most of this will be applicable to any entity which is owned by a tenant, not just inventories. The multi-tenant specific functionality can be broken out into a superclass.
Notice that our abstract TenantEntity
is a@MappedSuperclass
to prevent a table being generated for it and its@Inheritance
strategy will create a table per class that inherits from this object.
Inventory
now just has to concentrate on being inventory and the multi-tenancy code can be reused elsewhere through inheritance.
That takes care of database queries.
Creating, Modifying, and Deleting from the DB
Now the discriminator field must be set whenever a TenantEntity
is created, modified, or deleted. All three operations can be handled by one feature of Hibernate, an interceptor.
The interceptor provides hooks in the form of callbacks from the Hibernate session to your application code. By creating callbacks which check for TenantEntity
database objects being created, updated, or deleted, the tenantId
can be added or updated in the state of the TenantEntity
just prior to persistence without side effects on other types.
Creating the TenantInterceptor:
Upon I.) saving, II.) updating, or III.) deleting, the private helper IV.) addTenantIdIfObjectIsTenantEntity
is invoked and
- V.) checks if the intercepted object extends TenantObject and, if so,
- VI.) searches the property names on that object for one that equals
“tenantId”
. - VIII.) If the property is found, the corresponding index in the
state[]
array is set with the tenant identifier (we’ll figure out how to get this later). - IX.) Else, it will throw a new
ClassCastException
as all types extendingTenantEntity
should have a field matching“tenantId”
.
The core DAL modifications are done via leveraging a Hibernate filter and interceptor. Now it’s time to address our assumptions of data availability that we made at the start of the solution.
Getting the Tenant Identifier
The DAL is now set up to expect a tenant identifer in two places: passed to the TenantEntity
filter before querying the database and accessible from the TenantInterceptor
during creation, modification, and deletion.
A very quick and easy solution which could but shouldn’t be implemented would be to parse the identifer from the request and pass it as an argument down to the DAL. It shouldn’t be implemented as every method signature in the call stack would require an extra argument.
It also cannot be implemented as the methods in the TenantInterceptor
are invoked as callbacks from the Hibernate session. Additionally, the TenantInterceptor
must be able to call something static and thread safe in order to retrieve the identifer within the private helper method.
Based on these restrictions, the next simplest solution is to leverage the Spring SecurityContextHolder
which provides thread safe access to the SpringSecurityContext
which in turn holds an Authentication
object that contains the credentials of the user making the current request.² You can read about all of these here.
Modifying the TenantInterceptor:
In order to set the tenantId
property, the interceptor calls the SecurityContextHolder
to
- I.) get the current
SecurityContext
to - II.) get the current
Authentication
object which provides the tenant identifer by - III.) calling the
tenantIdentifier()
getter.
The Authentication
object does not have a property tenantIdentfier
, so AbstractAuthenticationToken
needs to be extended. Because this is a microservice environment, the extension of a more complex Authentication
implementation such as the AnonymousAuthenticationToken
is not required.³
Lombok’s @Data
annotation will provide a getter and setter method for the tenantIdentifer
field and the @NonNull
annotation will insert a null check at the top of the setter to ensure that there will always be a value.
There’s no need for getCredentials
or getPrincipal
to do anything yet, so they both return null for now.
Now the Authentication
object can be cast to a TenantAuthenticationToken
.
This cast will likely throw a ClassCastException
at some point. However, if there isn’t a TenantAuthenticationToken
loaded in the SecurityContext
, the request should not be processed, so this exception is acceptable for now.
Improvements and Next Steps
There is ample opportunity to improve the edge case and exception handling here. Both the TenantService
and TenantServiceAspect
(covered in part III) could depend on a TenantInformationService
to consolidate SecurityContext
access. The SecurityContext
could be extended and/or a custom SecurityContextStrategy
implemented to eliminate casting the Authentication
object. This is out of scope for the core features I’m discussing here, but I would highly encourage you to explore these possibilities if you’re putting this solution into production.
Look out for part III of this series in which we’ll look at how to implement the DAL modifications we solutioned here and how to set up the intra-microservice infrastructure to support the modifications.
- As with the entire solution, the provided example simply shows how to develop an approach to multi-tenancy. If you’d prefer not to use Hibernate, explore features of whatever frameworks you have access to that may offer similar functionality. Additionally, if you do choose to leverage Hibernate, be aware of the potential drawbacks of this approach.
- I stated in part I that multi-tenancy and authentication/authorization solutions should be independent of each other; leveraging spring’s security infrastructure to achieve multi-tenancy is simply reuse of existing infrastructure. It does not directly conflate the two and I want to reiterate the importance of keeping them distinct.
- This assumes that the microservices involved in this system are accessed through a gateway. User authentication should not have to be performed by every microservice, it should happen once at the gateway, so extending another
Authentication
implementation likeAnonymousAuthenticationToken
orUsernamePasswordAuthenticationToken
doesn’t make sense. All that matters is that whatever is contained in the token can be stored in theSecurityContext
.