Previously on the Multi Tenancy series:
- Multi Tenancy - Definition and scenario.
- Reviewing Litware HR - P&P Multi Tenancy Sample.
- Multi Tenancy - The Physical Data Model
- Multi Tenancy - Extensible Data Model
When talking about multi tenancy, one of the greatest challenges is to set the system in such a way that each tenant can define their own behaviors about the way the system behaves. In fact, this is my main criteria for whatever a system is truly a multi tenant app or simply has flexible schema.
This is a non trivial challenge to solve, because you need to have a solution that allows you to modify the behavior of the application for each individual tenant. Let us explore the options we have:
The Parameterize If Statement
This approach is extremely simple, you define a list of parameters that each tenant can modify, and you use an if statement to create the appropriate behavior based on the parameter value. As a simple example:
if tenant.pays_overtime: if tenant.max_overtime_hours is not null: salaryCalculation.PerformOvertimeCalculation(tenant.max_overtime_hours.Value); else: salaryCalculation.PerformOvertimeCalculation(int.MaxValue);
This approach is simple, as I said. But it suffer from a critical problem, scaling issues. The amount of parameters in the system rise sharply with its complexity, and the ability to understand how the system will behave for a particular tenant decrease just as rapidly. In fact, the matrix of options & values for the parameters tends to get out of control fast. The end result is a system where maintenance is extremely painful.
The Strategies and The Factory
One of the rules for refactoring, Replace Conditional with Polymorphism, is very applicable here. Instead of having a lot of ifs all over the place, we refactor them into a proper strategy pattern, so now we have this:
salaryCalculationStrategy = salaryFactory.CreateForTenant(tenant)
And we have implementations such as:
Now we only need to configure the appropriate strategies for a particular tenant, and we are done. If we want to understand the behavior of a particular tenant, we just review the strategies they use.
This almost works.
It is a huge step in the right direction, but the problem is that very often the change is not limited to behavior that can be handled using a small set of well known strategies. Most of the time, the changes that most tenants will want to make will not be identical from one tenant to another. The law of 80/20 applies here as well, and like in other places, everyone wants a different 20% changed.
The end result is that you end up with a lot of "generic" strategies that only really fit on tenant. Attempting to reuse those "generic" strategies will usually end in tears because of cascading affects when one tenant changes the required behavior but another doesn't.
The Interface, The Default Implementation and The Specialization, Oh My!
This approach calls for specifying the following: ISalaryCalculation and DefaultSalaryCalculation. DefaultSalaryCalculation is built with the intent of being used either as-is, or having derived classes modify and extend the behavior.
When a customer arrive with a required modification, we create a NorthwindSalaryCalculation that derives from DefaultSalaryCalculation and modify the behavior we need. If we require extensive modification, we may derive directly from ISalaryCalculation, rather than try to fit our behavior within the limits of the default implementation.
Note: I am only giving a single consistent example through this post, but when I am giving such an example, I am not referring to a specific service, consider what will happen if you have 50 such services, all of which a customer may choose to customize. This is where the real changes between the approaches come into play.
So far, I have only talked about very narrow definition of behavior, through services. It is actually a totally wrong way of looking at behavior. Mostly because it is giving us a very partial view of the way things are. We have behavior at the UI layer, we have our entities, which also have behavior, we have backend systems, etc.
I want to focus on the generic idea of behavior first, because it is an important concept, I'll talk about the specific in a later date.