Rails, the framework built on top of Ruby, just got its latest version(6.1) released. A lot of features and enhancements have gone into the latest version of Rails. You can read the for more details. official announcement I will be focusing particularly on the , what changed and how we can leverage Rails' native multi DB handling techniques for building scalable multi-tenant applications. Multi-DB improvements section was the first official rails version to support multiple databases. From the release notes: Rails 6.0 The new multiple database support makes it easy for a single application to connect to, well, multiple databases at the same time! You can either do this because you want to segment certain records into their own databases for scaling or isolation, or because you’re doing read/write splitting with replica databases for performance. Either way, there’s a new, simple API for making that happen without reaching inside the bowels of Active Record. The foundational work for multiple-database support was done by Eileen Uchitelle and Aaron Patterson. This allowed application developers to be able to define multiple database connections for a single application. Before this, developers had to use for any kind of multi DB support in Rails. Even though the ruby/rails community is very vibrant, third party gems often come with maintenance overheads with respect to upgrades, breaking changes, bugs, performance issues, etc. one of the many third party gems With Rails 6.0, you could define your in such a way: database.yml <%= %> # config/database.yml default: &default adapter: sqlite3 pool: ENV.fetch( ) { } "RAILS_MAX_THREADS" 5 timeout: 5000 development: primary: <<: *default database: primary_db primary_replica: <<: *default database: primary_db_replica replica: true animals: <<: *default database: animals_db animals_replica: <<: *default database: animals_db_replica replica: true Then define classes that could connect to these databases. ActiveRecord Abstract connects_to { , } connects_to { , } # app/models/application_record.rb # frozen_string_literal: true < ActiveRecord::Base class ApplicationRecord database: writing: :primary reading: :primary_replica end # app/models/animals_base.rb # frozen_string_literal: true < ApplicationRecord class AnimalsBase database: writing: :animals reading: :animals_replica end # app/models/user.rb # frozen_string_literal: true < ApplicationRecord class User end The abstract classes and models inheriting from them would both now have access to the method which can be used to establish connection to the configured database connections. connected_to ApplicationRecord.connected_to( ) User.do_something_thats_slow # some_controller.rb # frozen_string_literal: true role: :reading do end This approach worked great for primary-replica setup or setups where models had clear separation. i.e. a model always queried from a single database. However, with modern multi-tenant SaaS applications, horizontal sharding is almost a basic necessity. Depending on the tenant that's accessing the application, the application should be able to select which database it wants to query the data from. While how the application shards horizontally is DSL and can vary from a case to case basis, how it is able to connect to the underlying databases should be something that the framework should be able to handle. And so they did. With released in 6.1, you can now define shard connections for your abstract classes as well. The example from above changes as: Multi-DB improvements <%= %> # config/database.yml default: &default adapter: sqlite3 pool: ENV.fetch( ) { } "RAILS_MAX_THREADS" 5 timeout: 5000 development: primary: <<: *default database: primary_db primary_replica: <<: *default database: primary_db_replica replica: true animals: <<: *default database: animals_db animals_replica: <<: *default database: animals_db_replica replica: true animals_shard1: <<: *default database: animals_db1 animals_shard1_replica: <<: *default database: animals_db1_replica replica: true connects_to { , } connects_to { { , }, { , } } # app/models/application_record.rb # frozen_string_literal: true < ActiveRecord::Base class ApplicationRecord database: writing: :primary reading: :primary_replica end # app/models/animals_base.rb # frozen_string_literal: true < ApplicationRecord class AnimalsBase shards: default: writing: :animals reading: :animals_replica shard1: writing: :animals_shard1 reading: :animals_shard1_replica end # app/models/cat.rb # frozen_string_literal: true < AnimalsBase class Cat end Similar to 6.0, we can then leverage the method for switching(/establishing) connections to the configured databases. connected_to AnimalsBase.connected_to( , ) Cat.all # some_controller.rb # frozen_string_literal: true shard: :shard1 role: :reading do # reads all cats from animals_shard1_replica end One of the most common design patterns for multi-tenant architectures is to associate every tenant with a unique subdomain on your root domain. For eg. if your application runs on , marvel as a tenant would access the system using and so on. example.com marvel.example.com This pattern has its own advantages(easy/faster DNS resolution when running on a multi pod 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. To begin with, we will need a 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 and all data of that tenant would reside on that shard. So we will need a shard model as well. Tenant Shard We can begin by setting up the required database configurations first: <%= %> # 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. .abstract_class = db_configs = Rails.application.config.database_configuration[Rails.env].keys db_configs = db_file.each_with_object({}) db_key = key.gsub( , ) role = key.eql?(db_key) ? : db_key = db_key.to_sym configs[db_key] = {} configs[db_key][role] = key.to_sym connects_to db_configs .abstract_class = connects_to { , } ActsAsCurrent validates , { DOMAIN_REGEX } after_commit , private Shard.create!( .id, subdomain) ActsAsCurrent validates , { DOMAIN_REGEX } validates before_create private .shard = APP_CONFIGS[ ] # app/models/application_record.rb # frozen_string_literal: true < ActiveRecord::Base class ApplicationRecord self true do |key, configs| # key = default, db_key = default # key = default_replica, db_key = default '_replica' '' :writing :reading || end # connects_to shards: { # default: { writing: :default, reading: :default_replica }, # shard1: { writing: :shard1, reading: :shard1_replica } # } shards: end # app/models/global_record.rb # frozen_string_literal: true < ActiveRecord::Base class GlobalRecord self true database: writing: :default reading: :default_replica end # app/models/tenant.rb # frozen_string_literal: true < ApplicationRecord class Tenant include :subdomain format: with: # other DSL :set_shard on: :create def set_shard tenant_id: self domain: end end # app/models/shard.rb # frozen_string_literal: true < GlobalRecord class Shard include :domain format: with: :tenant_id :set_current_shard def set_current_shard self :current_shard #shard1 end end With multi-tenant architectures, there will always be a global context and a tenant specific context. We isolate such models through abstract classes and . They also take care of abstracting database connections and setting up the required isolations. ApplicationRecord GlobalRecord We can also leverage the for all models that belong to a tenant and inherit from . BelongsToTenant pattern ApplicationRecord unless another connection. Hence, when connecting to inherited models, we will not require any explicit connection handling. All ActiveRecord inherited models connect by default to a default shard and a writing role connected_to GlobalRecord We can also define a proxy class to abstract out all application specific connection handling logic: _connect_to ( , shard, &block) _connect_to ( , shard, &block) _connect_to ( GlobalRecord, , &block) private klass.connected_to( role, shard) block.call # app/proxies/database_proxy.rb # frozen_string_literal: true class DatabaseProxy << self class def on_shard ( , &block) shard: _ role: :writing shard: end def on_replica ( , &block) shard: _ role: :reading shard: end def on_global_replica (&block) _ klass: role: :reading 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 def _connect_to_ ( ApplicationRecord, , , &block) klass: role: :writing shard: :default role: shard: do end end end end With this setup in place, we can now write both application and background middleware that handle shard selection and tenant isolation on a per request or job basis. @app = app domain = env[ ] shard = Shard.find_by( domain) @app.call(env) shard shard.make_current DatabaseProxy.on_shard( shard.shard) account = Account.find_by( domain) account&.make_current @app.call(env) config.middleware.insert_after Rails::Rack::Logger, Middlewares::Multitenancy # lib/middlewares/multitenancy.rb # frozen_string_literal: true require 'database_proxy' module Middlewares # selecting account based on subdomain class Multitenancy def initialize (app) end def call (env) 'HTTP_HOST' domain: return unless shard: do subdomain: end end end end # config/application.rb require 'lib/middlewares/multitenancy' With more widespread adoption, the underlying framework should only get better from here. Native implementations also allow us better flexibility and control over code flows and application design. Also published on: https://dev.to/ritikesh/multitenant-architecture-on-rails-6-1-27c7