Same code – two platforms. With Rails. 

(Everyday Code – instead of keeping our knowledge in an README.md let’s share it with the internet)

We are running two platforms. FLLCasts and BuildIn3D. Both platforms are addressing entirely different problems to different sets of clients, but with the same code. FLLCasts is about eLearning, learning management and content management, while BuildIn3D is about eCommerce.

What we are doing is running both platforms with the same code and this article is about how we do it. The main purpose is to give an overview for newcomers to our team, but I hope the community could benefit from it as a whole and I could get feedback and learn what others are doing.

What do we mean by ‘same code’?

FLLCasts has a route https://www.fllcasts.com/materials. Results are returned by the MaterialsController.

BuildIn3D has a route https://platform.buildin3d.com/instrutions. Results are returned by the same MaterialsController.

FLLCasts has things like Organizations, Groups, Courses, Episodes, Tasks which are for managing the eLearning part of the platform.

BuildIn3D has none of these, but it has WebsiteEmbeds for the eCommerce stores to embed and put 3D building instructions and models on their E-commerce stores.

We run the same code with small differences.

Do we use branches?

No, we don’t. Branches don’t work for this case. They are hard to maintain. We’ve tried to have an “fc_dev” branch and a “b3_dev” branch for the different platforms, but it gets difficult to maintain. You have to manually merge between the branches. It is true that Git has made merging quite easy, but still it is an “advanced” task and it is getting tedious when you have to do it a few times a day and to resolve conflicts almost every time.

We use rails engines (gems)

We are separating the platform in smaller rails engines.
A common rails engine between FLLCasts and BuildIn3D is called fc-author_materials. It provides the functionality for and author to create a material both on FLLCasts and on BuildIn3D.

The engine providing the functionality for Groups for the eLearning part of FLLCasts is called fc-groups. This engine is simply not installed on BuildIn3D, we install it only on FLLCasts.

How does the Gemfile look like?

Like this:

install_if -> { !ENV.fetch('CAPABILITIES','').split(",").include?('--no-groups') } do
  gem 'fc-groups_enroll', path: 'gems/fc-groups_enroll'
  gem 'fc-groups', path: 'gems/fc-groups'
end

We call them “Capabilities”. By the default each platform is started with a “Capability” of having Groups. But we can disable them and tell the platform to start without Groups. When the platform starts the Groups are simply not there. We

How about config/routes.rb?

The fc-groups engine installs its own routes. This means that the main platform config/routes.rb is different from gems/fc-groups/config/routes.rb and the routes are installed only when the engine is installed.

Another option is to have an if statement and to check for capabilities in the config/routes.rb. We still have to decide which is easier to maintain.

Where do we keep the engines? Are they in a separate repo?

We tried. We have a few of the engines in separate repos. With time we found out it is easier to keep them in the same repo.

When the engines are in separate repos you have very strict dependencies between them. This proves to be useful but costs a lot in terms of development and creating a clear API between the engines. This could pay off when we would like to share the engines with the rest of the community like for example Refinery is doing. But, we are not there, yet. That’s why we found out we could spend the time more productively developing features instead of discussing which class goes where.

With the case of all the rails engines in a single repo we have the mighty Monolith again, we have to be grown ups in the team and maintain it, but it is easier than having them in different repos.

How do we configure the platforms?

FLLCasts will send you emails from team [at] fllcasts [dot] com

BuildIn3D will send you emails from team [at] buildin3d [dot] com

Where is the configuration?

The configuration is in the config/application.rb. The code looks exactly like this:

platform = ENV.fetch('FC_PLATFORM', 'fc')
if platform == 'fc'
  config.platform.sender = "team [at] fllcasts [dot] com"
elsif platform == 'b3'
  config.platform.sender = "team [at] buildin3d [dot] com"

When we run the platform we set and ENV variable called FC_PLATFORM. If the platform is “fc” this means “FLLCasts”. If the platform is “b3” this means “BuildIn3D”.

In the config/environments/production.rb we are referring to Rails.application.config.platform.sender. In this way we have one production env for both platforms. We don’t have many production evns.

Why not many production envs?

We found out that if we have many production envs, we would also need many dev envs and many test envs and there will be a lot of duplication between them.

That’s why we are putting the configuration in the application.rb. It’s about the application, not the environment.

How do we deploy on heroku?

First rule is – when you deploy one platform you also deploy the other platform. We do not allow different versions to be deployed. Both platforms are running the same code, always. Otherwise it gets difficult.

When we deploy we do

# In the same build we 
# push the fllcasts app and then the buildin3d app
git push fllcasts production3:master
heroku run rake db:migrate --app fllcasts

git push buildin3d production3:master
heroku run rake db:migrate --app buildin3d

In this way both platforms always share the same code, except for a short period of a few minutes between the deployment.

How are the views separated?

The platforms share a controller, but the views are different.

The controller should return different views for the different platforms. Here is what the controller is doing.

def show
   # If the platform is 'b3' return a different set of views
    if Rails.application.config.platform.id == "b3"
      render :template => Rails.application.config.platform.id+"/materials/show"
    else
      render :template => "/materials/show"
    end
  end

In the same time we have the folders:

# There are two separate folders for the views. One for B3 and one for FC 
fc-author_materials/app/views/b3/materials/
fc-author_materials/app/views/materials/

How do we test?

Testing proved to be challenging at first. Most of the time as the code is the same the specs should be the same right?

Well, no. The code is the same, but the views are different. This means that the system specs are different. We write system and model specs and we don’t write views and controllers specs (if you are still writing views and controllers specs you should consider stopping. They were deprecated years ago).

As the views are different the system specs are different.

We tag the specs that are specifically for the BuildIn3D platform with a tag platform:b3


  context "platform is b3", platform: :b3 do
    before :each do
      expect_b3
    end

When we run the specs we run first all the specs that are not specifically for b3 with

$ rake spec SPEC="$specs_to_build" SPEC_OPTS="--tag ~platform:b3 --order random"

Then we run a second suite for the tests that are specifically for the BuildIn3D platform.

# Note that here we set an ENV and we use platform:b3 and not ~platform:b3
$ FC_PLATFORM="b3" rake spec SPEC="$specs_to_build" SPEC_OPTS="--tag platform:b3 --order random"

I see how this will become difficult to maintain if we start a third platform or a fourth platform, but we would figure it out when we get there. It is not something worth investing any resources into as we do not plan to start a new platform soon.

Conclusion

That’s how we run two platforms with the same code. Is it working? We have about 200 deployments so far in this way. We see that it is working.

Is it difficult to understand? It is much easier than different branches and it is also much easier than having different repos.

To summarize – we have a monolith app separated in small engines that are all in the same repo. When running a specific platform we install only the engines that we need. Controllers are the same, views could be different.

I hope this was helpful and you can see a way to start a spin off of your current idea and to create a new business with the same code.

There is a lot to be improved – it would be better to have each and every rails engine as a completely separate project in a different repo that we just include in the platform. But we still don’t have the requirement for this and it will require months of work on a 10 years old platform as ours. Once we see a clear path for it to pay off, we would probably do it in this way.

For fun

Thanks for stopping by and reading through this article. Have fun with this 3D model and building instructions.

GeoShpere3 construction with GeoSmart set