Elegant Multi-Tenancy for Microservices — Part I: Why Bother?
An End-To-End Solution Your Team Will Love by Jakob Rodseth
Why Bother?
Back in 2016, Lyft rolled out Lyft Line in San Francisco. The motivation behind Line lay in the opportunity for reuse through sharing: “over 80% of rides shared a significant portion of their route with another ride”.¹ Lyft seized an opportunity for scaling by utilizing each ride’s vertical resources (the number of seats in each car). They made each ride capable of serving multiple tenants at once.
If you’re serious about building a horizontally scalable distributed system, it absolutely must be multi-tenant.
The reasoning for engineering multi-tenant software systems is even more compelling than ridesharing: in most cases, the opportunity for system reuse between tenants will be at or near 100%. It’s not necessary to spin up service instances per-tenant, significantly reducing operating costs. Whitelabeling applications while retaining all source code is a cinch. Production deployments run over one environment, not one hundred. If you’re serious about building a horizontally scalable distributed system, it absolutely must be multitenant.
In the following series, I will outline a comprehensive multi-tenancy solution using an imaginary Spring Boot microservice system for context. However, the philosophy of the approach should be applicable to any framework or language. I’ll be emulating an XP development style and thought process throughout the discussion in order to demonstrate necessity driven decision making as well as offer a deep explanation of how the components synergize to create a compelling, modular architecture.
If you’re looking for lots of bells and whistles, you won’t find them here; this is a lean set of tools that will enable your team to implement an MVP multi-tenancy framework quickly, punting extra functionality in favor of maximizing flexibility, reuse, and abstraction.
Your team will love it.
Adding Value and Cost Of Change
Break the rules — Multi-tenancy is not any more a feature than your CI/CD pipeline, message queues, or security paradigms are features.
I work in an Agile environment every day; TDD, pairing, iteration planning, the works. A large portion of the paradigms and disciplines that Integral, our clients, and I practice revolve around high frequency delivery of value via deployment of production code. Traditional Agile thinking may tempt you to delay implementation of a “value-adding-feature” like multi-tenancy until you actually have a second or even third tenant to justify it. Break the rules — Multi-tenancy is not any more a feature than your CI/CD pipeline, message queues, or security paradigms are features.
The cost of change when you delay multi-tenancy past your first tenant is astronomic. By not planning support for multiple tenants within single instance from day one, your team is signing up for hours of risky production data migrations, systemic changes to every single service and data entity, and potential backwards incompatibility with clients or third-party systems. It’s not worth the cost of change to delay multi-tenancy, not to mention the fiscal costs of hosting and operating multiple instances.
Goal and Anti-Goals
An end-to-end MVP framework for implementing multi-tenancy achieves one thing:
1.) As a user associated with a particular tenant of the system, the only data I interact with belongs to my tenant.
That’s it! Anything else is extraneous to multi-tenancy, including permissions.
Notice that the goal does not state that the “only data I am allowed to interact with”. It’s very tempting to bundle your permissions and multi-tenancy solutions together; if I am a user associated with a particular tenant, then it is accurate to say that I have permission to interact with that tenant’s data. Although this is an intuitive and an initially attractive direction, it misses the point of multi-tenancy.
It should be both practically and technically impossible for Tenant A’s users to be aware of Tenant B’s existence. No mechanism should exist through which one of Tenant A’s users could gain access to Tenant B’s data. If you mix permissions and multi-tenancy, all it takes to leak data is for a Tenant A user to be assigned a permission through error or malice. Instead, if permissions and multi-tenancy are separated, the permissions layer can live on top of the tenancy layer, strengthening security and distribution of responsibility.
Our first anti-goal is therefore:
A.) Achieving multi-tenancy through mechanisms other than the data access layer (DAL).
The ramification of this anti-goal is that a solution that exists outside of each microservice, i.e. a gateway filter which inspects permissions, is not desirable. The solution must be implemented in the DAL within each microservice. Creating a completely unique implementation specific to each microservice is a huge waste of time and reduces maintainability.
Our second anti-goal is therefore:
B.) Implementing many microservice-specific solutions.
By compositing our goal and anti-goals, a clear MVP emerges. We will know we are done when we have:
(B.) A standalone, generalized package that can be imported as a dependency for any microservice which (A.) modifies the data access layer so that, (1.) as any user associated with a particular tenant of the system, the only data I interact with belongs to my tenant.
Design Decisions
Let’s imagine we’re creating a microservice that will present a RESTful API for managing a car factory’s inventory. We can create and add different kinds of car parts to the factory inventory, modify them as they’re assembled into a vehicle, and remove² them once they roll out of the factory as a brand new car. This service can be used to manage many inventories of many different tenants.
First, let’s pretend we already have two inventories filled with car parts, one belonging to Tenant A and one to Tenant B.
How are these inventories stored?
You can read about different database tenancy patterns here, but there are three basic multi-tenancy relational database patterns: Separate databases, separate schemas, or a discriminator field. You should choose the model that works best for your business, architecture, security, and scalability needs, but a discriminator field is the most simple and results in the most manageable architecture. One database means data maintenance and conversion is easy and it’s cheaper than having a database per client.
Therefore, let’s decide that the inventories are stored together in one relational database.
The next question is how can we discriminate between Tenant A and Tenant B’s data?
One option would be to put the discriminator field on each of the car parts in the database. However, we’ve already stated that this microservice handles inventories and that car parts only make sense within the context of an inventory. Car parts therefore must belong to an inventory, making inventories a top level collection. From an OOD perspective, the inventory is the thing belonging to a tenant while the car parts belong to an inventory. If this microservice included the concept of a factory, each factory having multiple inventories, the factory would be the top level collection.
Top level collections, in this case inventories, are therefore marked with a discriminator field to reduce data replication and adhere to normalization. Let’s assume that the discriminator field is the identifier of the tenant which owns the inventory and that any request to get an inventory provides a tenant identifier. How can we ensure that only inventories with the tenant identifier provided by each request are retrieved?
With ample context established and most of the broad decisions made, this is a great place to start solutioning.
In Summary
This was a high level overview of the benefits of multi-tenancy, its necessity, its MVP definition, and a thought experiment guided design process. Watch out for part II of this series for a deep dive into formulating a solution to achieve our goal, starting by addressing the question of how to discriminate data via a tenant identifier.
- Matchmaking in Lyft Line — Part 1
- This operation should be a soft delete. Hard deletion is used for the sake of demonstrating a comprehensive solution.