Overgrown Tenant class decomposition

Piotr Jurewicz's photo
Piotr Jurewicz

Published on Jan 11, 2022

2 min read

Subscribe to my newsletter and never miss my upcoming articles

If you have been working on a multi-tenant application for a long time, there is a high chance that your Tenant model has grown to an enormous size.

You probably started with id, name, and subdomain columns.

Later there were things that had to be configured per tenant. So you simply added a bunch of columns.

The project started to grow rapidly and you added more flags, external API credentials, and even frontend configuration. You did it automatically. Before you knew it, you had several dozen columns with names similar to "sales_page_menu_text_hover_color" or "proforma_invoices_generated_automatically".

That's literally my case... My Tenant class had above 30 associations defined, 20 validations, and 8 enums. Almost 500 lines of code. It is mapped to 190 columns. All of this data was loaded at the very beginning of basically each request for the purpose of setting current_tenant where only the id is necessary to limit other queries scopes...

I wasn't interested in splitting the tenant configuration across multiple tables for that moment. I just wanted to reduce the amount of redundant data being constantly loaded and do some decomposition for better maintainability.

I limited the number of columns being selected for setting the current_tenant.

class ApplicationController < ActionController::Base
  # ...
  def set_tenant
    tenant = System.select(:id).find_by_subdomain(request.subdomain)
    set_current_tenant(tenant)
  end
end

I introduced a generic TenantConfiguration class.

class TenantConfiguration < ApplicationRecord
  self.abstract_class = true
  self.table_name = 'tenants'

  default_scope { select(['id'] + const_get('COLUMNS')) }

  def self.for_tenant(tenant)
    self.find(tenant)
  end
end

And created a specific class for each configuration area, for example:

class LumpTaxConfiguration < TenantConfiguration
  COLUMNS = [:lump_taxpayer, :payment_fee_lump_code, :delivery_lump_code]
  # ...
end

Whenever I really need a specific configuration, I just fetch it explicitly, by invoking:

LumpTaxConfiguration.for_tenant(current_tenant).lump_taxpayer?

I believe It is a step in a good direction. What is your opinion? Do you see a problem with the fat Tenant model in your multi-tenant projects? How do you cope with it?

 
Share this