Design a multitenant application on Rails 6 with horizontal sharding
Tags: rails, programming, webdev, database
Published: 2020-12-25 08:44:47

---
title: Design a multitenant application on Rails 6 with horizontal sharding
published: true
description: How to leverage Rails 6.1 to design a multitenant application with horizontal sharding
tags: rails,programming,webdev,database
cover_image: https://rubyonrails.org/images/rubyrails.png
series: Multitenant applications using Rails 6.1
---

One of the most common design patterns for multitenant architectures is to associate every tenant with a unique subdomain on your root domain. For eg. if your application runs on [example.com](#), marvel as a tenant would access the system using [marvel.example.com](#) and so on. 

This pattern has its own advantages(easy/faster DNS resolution when running on a multi pod <Point of Delivery> setup) and disadvantages(DNS updates for every tenant creation). Instead of debating that, we will delve into how to implement this architecture in a Rails application [using the new multi & horizontal DB setup provided by Rails 6.0/6.1](https://dev.to/ritikesh/multitenant-architecture-on-rails-6-1-27c7).

To begin with, we will need a `Tenant` model. Since your tenants will be identified by subdomains, it makes sense to have a subdomain column in the table along with other application required attributes. Each tenant belongs to a `Shard` and all data of that tenant would reside on that shard. So we will need a shard model as well.

We can begin by setting up the required database configurations first:
```yaml
# config/database.yml

default: &default
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  default:
    <<: *default
    database: primary_db
  default_replica:
    <<: *default
    database: primary_db_replica
    replica: true
  shard1:
    <<: *default
    database: shard1_db
  shard1_replica:
    <<: *default
    database: shard1_db_replica
    replica: true
```
We will define the required models as well accordingly.
```ruby
# app/models/application_record.rb

# frozen_string_literal: true
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  db_configs = Rails.application.config.database_configuration[Rails.env].keys

  db_configs = db_file.each_with_object({}) do |key, configs|
    # key = default, db_key = default
    # key = default_replica, db_key = default
    db_key = key.gsub('_replica', '')
    role = key.eql?(db_key) ? :writing : :reading

    db_key = db_key.to_sym
    configs[db_key] ||= {}

    configs[db_key][role] = key.to_sym
  end

  # connects_to shards: {
  #   default: { writing: :default, reading: :default_replica },
  #   shard1: { writing: :shard1, reading: :shard1_replica }
  # }
  connects_to shards: db_configs
end

# app/models/global_record.rb

# frozen_string_literal: true
class GlobalRecord < ActiveRecord::Base
  self.abstract_class = true

  connects_to database: { writing: :default, reading: :default_replica }
end

# app/models/tenant.rb

# frozen_string_literal: true
class Tenant < ApplicationRecord
  include ActsAsCurrent

  validates :subdomain, format: { with: DOMAIN_REGEX }
  # other DSL

  after_commit :set_shard, on: :create

  private

  def set_shard
    Shard.create!(tenant_id: self.id, domain: subdomain)
  end
end

# app/models/shard.rb

# frozen_string_literal: true
class Shard < GlobalRecord
  include ActsAsCurrent

  validates :domain, format: { with: DOMAIN_REGEX }
  validates :tenant_id

  before_create :set_current_shard

  private

  def set_current_shard
    self.shard = APP_CONFIGS[:current_shard] #shard1
  end
end
```
With multitenant architectures, there will always be a global context and a tenant specific context. We isolate such models through abstract classes `ApplicationRecord` and `GlobalRecord`. They also take care of abstracting database connections and setting up the required isolations.

We can also leverage the [`BelongsToTenant` pattern](https://dev.to/ritikesh/simple-multitenancy-inrails-42b6) for all models that belong to a tenant and inherit from `ApplicationRecord`.

[All `ActiveRecord` inherited models connect by default to a `default` shard and a `writing` role](https://github.com/rails/rails/blob/60d5928a27c5db850f2b258a1484bf5ad7d1d302/activerecord/lib/active_record/core.rb#L235) unless `connected_to` another connection. Hence, when connecting to `GlobalRecord` inherited models, we will not require any explicit connection handling. 

We can also define a proxy class to abstract out all application specific connection handling logic:
```ruby
# app/proxies/database_proxy.rb

# frozen_string_literal: true
class DatabaseProxy
  class << self
    def on_shard(shard: , &block)
      _connect_to_(role: :writing, shard: shard, &block)
    end

    def on_replica(shard: , &block)
      _connect_to_(role: :reading, shard: shard, &block)
    end

    def on_global_replica(&block)
      _connect_to_(klass: GlobalRecord, role: :reading, &block)
    end

    # for regular executions, since Global only connects to default shard,
    # no explicit connection switching is required.
    # def on_global(&block)
    #   _connect_to_(klass: GlobalRecord, role: :writing, &block)
    # end

    private

    def _connect_to_(klass: ApplicationRecord, role: :writing, shard: :default, &block)
      klass.connected_to(role: role, shard: shard) do
        block.call
      end
    end
  end
end
```
With this setup in place, we can now write both application and background middlewares that handle shard selection and tenant isolation on a per request or job basis.
```ruby
# lib/middlewares/multitenancy.rb

# frozen_string_literal: true
module Middlewares
  # selecting account based on subdomain
  class Multitenancy
    def initialize(app)
      @app = app
    end

    def call(env)
      domain = env['HTTP_HOST']
      
      shard = Shard.find_by(domain: domain)
      return @app.call(env) unless shard

      shard.make_current
      DatabaseProxy.on_shard(shard: shard.shard) do
        account = Account.find_by(subdomain: domain)

        account&.make_current
        @app.call(env)
      end
    end
  end
end

# config/application.rb
require 'lib/middlewares/multitenancy'

config.middleware.insert_after Rails::Rack::Logger, Middlewares::Multitenancy
```

Anybody who's building new products on the web, Ruby on Rails has never been better to kickstart your next big unicorn.