How to switch a Solidus eCommerce to Multi-tenant

Flavio Auciello

21 Sept 2018 Development, Solidus, eCommerce, Ruby On Rails

Flavio Auciello

6 mins
How to switch a Solidus eCommerce to Multi-tenant

for extracting data from each schema. This approach is cheaper than multi-db because it requires less hardware, time and resources to manage a large number of customers.

Shared schema

In this implementation the tenants' records share the same tables within the same database, using an ad-hoc column in each table to discriminate among all tenats' records.

There is no data isolation and no fault tolerance: every problem with our persistent layer affects on all tenants; all backups, restores, extraction operations for a single tenant do not come for free: you should implement all these mechanisms by yourself at the application level because you could have different policies for different customers.

Just like the multi-schema approach we'll face less costs in terms of hardware in the long period but considerable development costs of a shared-architecture.

Which approach should I use?

The answer is always the same: it depends. When you choose a solution you have to face several trade-offs. This article provides some interesting considerations.

As a rule of thumb it's better avoid a multi-db approach if you don't know for sure how many tenants you're about to manage. Avoid a shared approach if you expect a huge amount of data per tenant or when particular capabilities are required for each tenant.

Choose the right tool for the right job.

Within Rails

Now back to the topic of this blogpost: how to migrate our newly created Solidus eCommerce to multi-tenant?

First of all, Solidus is a Rails app and achieving multi-tenancy within Ruby on Rails is quite easy due to the highly flexible and extendible nature of the Ruby language and Rails' engine-based architecture. After all, that's one of the main reason we work with Solidus!

I recommend this old-but-gold Railscast that shows how to implement a shared-schema multi-tenant architecture by yourself using ActiveRecord scopes.

However there are already a bunch of gems implementing all types of multi-tenancy.

Apartment

There is an amazing, mature - and veeery well documented - gem made by Influitive Corp. called Apartment that will implement both multi-db and multi-schema architectures pretty well and almost out-of-the-box. This gem basically introduces a Rack middleware (called Elevator) that is responsible of intercepting the requests, selecting the target tenant according to some criteria (like the domain/subdomain name or, for example, the authenticated user) and preparing ActiveRecord queries with all the stuff needed to switch to the correct database or schema.

There are a lot of articles on how to migrate your RoR application to multi-tenant using the Apartment gem. We really appreciated Troy Anderson's Zero to Multi-Tenant in 15 Minutes – A Rails Walkthrough, and this blogpost by Brad Robertson. There are also some useful screencasts (see Credits).

Within Solidus

As mentioned above Apartment basically interacts with ActiveRecord and the Rack middleware system on which Rails relies on, so should work pretty well together with Solidus. And so it is! However, there are some known issues we have to keep in mind when switching a Solidus app to multi-tenant.

In fact, as the application complexity and dependencies grow, we could face some conflicts or issues, expecially when we use some specific or intrusive gems. Here I'll summarize the most common I found around the web. I've created this tutorial project that aims to serve two different online stores: www.pescara-shop.com and www.latina-shop.com. The project relies on the Apartment gem and adopts a multi-schema implementation. This project has a good README and a list of self-explaining commit messages you can follow as step-by-step instructions. Here you can find more details about the encountered issues and some advice on how to make Solidus and Apartment work.

Preferences

Since Solidus preferences could be persisted in the database (legacy_db_preferences mode), we could store the preferences for each tenant when the application starts and retrieve the correct preferences with each request, depending on tenant. Unfortunately, saving preferences on database is currently deprecated and it might be removed:

Replace the default legacy preference store, which stores preferences in the spree_preferences table, with a plain in-memory hash. This is faster and less error-prone. But using a plain in-memory hash requires some workarounds to properly store the preferences.

Testing

Since Solidus uses RSpec, I've focused my analysis on how this testing suite works with the Apartment gem. Let's start by looking at the Apartment wiki page. Here we can find most of the instructions and the best practices we need:

The number one thing that has helped us in testing is to create a single tenant before the whole test suite runs and switch to that tenant for the run of your tests. In fact, using feature specs is almost impossible, and as far as I know, the reason is the same why we cannot use transactional tests with Capybara:

Transactional fixtures do not work with Selenium tests, because Capybara uses a separate server thread, which the transactions would be hidden from. We hence use DatabaseCleaner to truncate our test database. See this issue for more details.

The combination of the mandatory use of DatabaseCleaner and the need to test against a single test-tenant, produces a quite standard configuration like this:

# spec/rails_helper.rb
RSpec.configure do |config|
  config.use_transactional_fixtures = false
  config.before(:suite) do
    # Clean all tables to start
    DatabaseCleaner.clean_with :truncation
    # Use transactions for tests
    DatabaseCleaner.strategy = :transaction
    # Truncating doesn't drop schemas, ensure we're clean here, app *may not* exist
    Apartment::Tenant.drop('test_app') rescue nil
    # Create the default tenant for our tests
    Apartment::Tenant.create('test_app')
  end
  config.before(:each) do
    # Start transaction for this test
    DatabaseCleaner.start
    # Switch into the default tenant
    Apartment::Tenant.switch! 'test_app'
  end
  config.after(:each) do
    # Reset tentant back to `public`
    Apartment::Tenant.reset
    # Rollback transaction
    DatabaseCleaner.clean
  end
end

Seeding

I reccomend using Rails' plain-old seeds mechanism instead of external gems. One of the most famous is Seedbank, but it doesn't seem to be working with Apartment (and lacks support for Rails 5). A little code has to be written in order to setup a very basic tenant-based seed loading mechanism. Something like:

# config/db/seeds.rb
NebulabShop::Stores.each do |tenant, domain|
  Apartment::Tenant.switch(tenant) do
    Rails.root.join("db/seeds/#{tenant}").each_child do |seed_file|
      puts "Loading seed file: #{seed_file}"
      load seed_file
    end
  rescue Errno::ENOENT => e
    puts "there was an error while accessing #{e.message}"
  end
end

Asynchronous jobs

Sidekiq is probably the best known background processor for Ruby and Rails. Although it's not a gem included in Solidus there's plenty of situations where you probably need asynchronous jobs. Luckily, as mentioned in the Sidekiq wiki

You can use client middleware to add job metadata to the job before pushing it to Redis. The same job data will be available to the server middleware before the job is executed, if you need to set up some global state, e.g. current locale, current tenant in a multi-tenant app, etc it's possible to exploit the middleware architecture of Sidekiq to inject tenant-switching logic into the Sidekiq workflow. Moreover, there's no need to implement this by yourself! Influitive developed an Apartment adapter for Sikdekiq called apartment-sidekiq.

Conclusions

In this blogpost I've briefly introduced the concept of multi-tenancy and investigated on how the adoption of this architecture could impact our Solidus application, giving a sum up of some useful resources I've found along the way. I've highlighted the amazing extendable architecture of the framework which allows us to highly customize our business logic and behaviors - and so multi-tenancy - by simply decorating Solidus objects. If you're interested in a more deep dive into implementation or configuration details, you can refer to the Multi-tenant shop project. Hope this helped you! Feel free to comment or ask for any questions.

References and Credits

Articles

Screencasts

Books

You may also like

Let’s redefine
eCommerce together.