• Follow us

How to switch a Solidus eCommerce to Multi-tenant

Flavio auciello

Flavio Auciello

on 21 Sep 2018 in #Code

12 minutes Read
How to switch a Solidus eCommerce to Multi-tenant

In this blogpost I’ll show how to migrate a Solidus app from single to multi-tenant. But what is multi-tenancy? Here I’ll give a little introduction on what multi-tenant applications really are, how they can be implemented and the pros and cons of the different approaches.

First of all let’s take a look at a couple definitions about multi-tenancy around the web:

Multi-tenancy is an optimization for hosted services in which multiple customers are consolidated onto the same operational system, a technique pioneered by salesforce.com.

A Multi-tenant solutions enable an innovative business strategy for enterprises because they allow development and maintenance costs to be shared.

With this architecture a single application instance can serve multiple customers, each with its own information base, UX, and also behaviors, up to a certain point.

An eCommerce is a typical example of SaaS application in which multi-tenancy is largely adopted (Shopify, Salesforce), expecially for a store dealing with the B2B2C business model.

Imagine a seller who has a couple of stores who wants them branded somewhat differently. We’re actually serving subgroups of users with more or less the same purpose and business logic (we want to sell products to them!), but each of them with its own domain name, products, campaigns and promotions and -at a more technical level- with its own data store, web interface, asyncronous jobs, and so on. We could have dozens of these stores. It’s quite clear that a multi-istance approach in this case is unaffordable. By the way, modern virtualization clustering and deployment tools have drastically simplified these kind of procedures.

Multi-tenant implementations

A multi-tenant architecture could be implemented in three ways, according to the level of data isolation we want:

  1. Multi-DB (shared-machine or shared-instance)
  2. Multi-schema (shared-process)
  3. Shared schema

For each of these patterns there are some pros/cons you should take care about, in particular:

  1. data isolation and fault tolerance
  2. backup and maintenance
  3. costs

Multi-DB

In this implementation, each application instance uses a separate database for each tenant.

The main advantage of the “one database ↔ one tenant” design is that it ensures the highest level of data isolation (one tenant simply cannot access another tenant’s data) and fault-tolerance (a DB fault will only affect one tenant).

Each tenant can be easily backed up by dumping an entire database.

This implementation costs in resources overhead as each database has its own connection pool and memory context. It also requires an extra amount of time spent to setup and manage potentially thousands of databases and a series of ad-hoc tools and scripts are needed to achieve cross-tenant data extraction and analysis. However nowadays we have great tools to easily manage database clusters, and manage data extraction via modern techniques.

Sometimes this choice is mandatory due to legal issues related to data isolation or because you have to accomplish strong Service Level Agreements. Consider using this implementation if you want to have complete flexibility: imagine a situation where one tenant wants to encrypt data but others don’t.

Multi-schema (or shared-process)

With this implementation we could use the same database for all tenants, but giving each tenant their own tables. The shared-process implementation is made possible thanks to a feature offered by some database drivers: schemas. There are plenty of database drivers supporting this feature, like Amazon Redshift, DB2, PostgreSQL, Presto, Snowflake, SQL Server, Vertica and many others. The schema is a sort of a namespace for tables. The database selects the target table using the namespace in a way similar to what the UNIX shell does when using the PATH environment variable. Take a look here to see how PostgreSQL implements this mechanism.

Less isolation means we have to face the old Single Point Of Failure, as every db operation affects all tenants.

Data extraction is not very expensive, and database vendors usually offer tools 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 approcach 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 appilication 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 let 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

Still not sure which solution fits best your situation?

Let's talk
Related posts
#Code | 29 Feb 2016

Why Solidus?

Spree or Solidus? Which OSS project Nebulab, as an agency involved in Ruby on Rails eCommerce development, should invest on? This blog post...

Join the Conversation