Rebuilding shutl.com using microservices and layer-7 load balancing
A few weeks ago we released a brand new version of shutl.com with our new branding, an updated look and clearer messaging. We also added the ability to book a Shutl delivery right from the homepage as we work to consolidate our various web offerings.
We decided to follow a microservices pattern, instead of building one monolithic application, meaning that although visitors would see one site on a single domain with a consistent look and feel, behind the curtain there are serveral applications running.
One service handles the core functionalities of the website, like the overview, the retailers and carriers sections, “the contact us” section etc. For feedback and ratings pages, the HTML is served by a different service (Feedback service), and the same for the tracking pages which are provided by Tracking service. All services are Rails applications that are configured to use the Unicorn application server, with Nginx acting as a layer 7 load balancer.
When you have a website structured like ours, the way you render pages might need a little bit of more work and thinking compared to a monolithic application. In the next paragraphs I’ll talk about two approaches to handle page rendering in a microservices based website.
Achieve 100% decoupling
Although many services means having many pages spread across different applications, it’s fundamental to keep the users under the same domain when they access different website pages. We want to basically give the users the illusion of accessing a single web application, while keeping all the single services completely decoupled.
A common way to achieve that, which we also apply at Shutl, is to leverage Nginx as a reverse proxy to forward the request to the right service, based on the request url.
The configuration below shows how Nginx can be configured to achieve what I just described. That configuration is very close to what we use in production to render to reviews page (https://shutl.com/reviews)
upstream frontend_app { server unix:/opt/shutl/unicorn/.unicorn.sock fail_timeout=30; } upstream feedback_service { server feedback_service.com:80 fail_timeout=300; } server { listen 80 default_server; server_name shutl.com; root /opt/frontend_app/public; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header Host $host; location /reviews { proxy_pass http://feedback_service; } location @app { proxy_pass http://frontend_app; } }
The lines 17-19 are the most relevant ones, needed to instruct Nginx so that if a request url is ‘shutl.com/reviews’, the request is forwarded to the URL given by the proxy_pass directive, that in our case is the feedback_service upstream (lines 5-7). That means that the Nginx listening to the port 80 of the feedback_service.com server will receive a request like the following: feedback_service:80/reviews. Feedback service will then handle the request and respond with the html page containing the reviews that the user will eventually enjoy at https://shutl.com/reviews.
Embeddable snippets
Some pages are composed with content from several services. For example, our homepage shows customer reviews, streamed live and unedited. In this case, one application is serving the overall page, but it fetches HTML snippets from another – Feedback service. To create this section, we added an endpoint to the Feedback service API that returns a HTML fragment containing the latest reviews. That snippet will be fetched and rendered whenever the overview page is showed to the user.
Let’s dig into some code and see how that has been implemented.
Here’s the Rails route file:
Rails.application.routes.draw do root 'site_pages#show', file_name: 'overview' get ':file_name', to: 'site_pages#show' end
The routes file shows that when a request to the root page is received, the file_name parameter will be set to ‘overview’, and the request will be handled by the show action of a controller called SitePagesController, that will look like the one below:
class SitePagesController < ApplicationController helper :reviews def show template = File.join(params[:controller], params[:file_name]) render template end end
It’s a very concise and simple controller. It assigns the template’s file path (site_pages/overview) to the template variable, which it’ll be used to render the desired Haml template.
The template will contain, among other information, the recent reviews which will be fetched from the Feedback service by a helper, so not to clutter the controller too much and keep a nice separation of concerns.
The helper will look something like:
module ReviewsHelper def render_latest_reviews response = fetch_from_feedback_service('/latest_reviews') response.body.force_encoding('UTF-8').html_safe end private def fetch_from_feedback_service(url) connection.get do |req| req.url url end end def connection Faraday.new(url: 'http://feedback_service.com') end end
This example is simplified – in reality we also want to handle error responses, timeouts and other exceptions. What the helper does should be quite clear to all of you. The method render_latest_reviews will call the fetch_from_feedback_service method to fetch the HTML snippet containing the latest reviews, that will be embedded in the Overview page.
The only missing piece is the overview template that will just call the render_latest_feedbacks method (available in the ReviewsHelper) to show the latest reviews:
%div %span What our customers are saying %div = render_latest_feedbacks
The approach just described can be applied whenever you need to embed a chunk of information (coming from a different service), in different pages of the same website, across several applications, or in the emails that you will send to the customers as part of a marketing campaign.
In the past we have used a more traditional approach – receive the data needed from the external service as JSON, and render that into HTML in the calling application. We prefer receiving the HTML from the external service as it keeps the display logic for the application within that application, meaning it can be easily changed. This is preferable to having duplicate logic in several places, in different applications, for rendering the same data. We share assets between applications to keep the styling and user experience consistent.
We’re proud of the new site and had a good time building it; we gave ourselves a deadline and were ruthless with scope to make that happen. We’re happy with the result – a great looking site that allows us to iterate and test new ideas quickly. Let us know what you think!