Elegant Multi-Tenancy for Microservices — Part II: Solutioning

An End-To-End Solution Your Team Will Love by Jakob Rodseth

Integral.
5 min readFeb 18, 2018

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 type Long 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 extending TenantEntity 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.

  1. 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.
  2. 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.
  3. 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 like AnonymousAuthenticationToken or UsernamePasswordAuthenticationToken doesn’t make sense. All that matters is that whatever is contained in the token can be stored in the SecurityContext.

--

--

Integral.

Detroit based and focused on software excellence. Inclusion, transparency, bias to action, feedback loops, experimentation matter to us.