From MVP to SOA, a journey in scaling

I’m going to look at one sliver of functionality and how it can be scaled to cope with greater and greater demand starting at the MVP stage, to viable startup and ending in profitability and webscale.

The use case I will look at is sending email when a bid is placed in a fictious auction website.

Entity

class Bid < ActiceRecord::Base
  belongs_to :buyer, class_name: Customer
  
  def self.using_form(form)
    new(form.attributes)
  end
end

Our model has no validations. This is so we don’t restrict ourself with regards to what is considered a valid entiry. It is pretty much a simple data object.

Form

class Bids::Create::Form
  include ActiveModel::Model
  include Virtus
  
  attribute :buyer_id,    Integer
  attribute :auction_id,  Integer
  attribute :maximum_bid, Money
  
  validates :buyer_id,    presence: true
  validates :auction_id,  presence: true
  validates :maximum_bid, presence: true
end

We create a “form” which to capture, validate and sanatize user input which will be used to create a new Bid entity.

Service object

class Bids::Create
  include Wisper::Publisher
  
  def call(form)
    if form.valid?
      bid = Bids::Bid.using_form(form)
      bid.save!
      
      broadcast(:create_bid_successful, bid.id)
    else
      broadcast(:create_bid_failed, bid.id)
    end
  end
end

The service will take the form and use it to create a new entity. It will broadcast an event to signal the outcome.

Controller

class Bids::CreateController < ApplicationController
	def new
	  @form = new_form
	end

	def create
	  @form = new_form
	  create_bid = Bids::Create.new(@form)
	  
	  create_bid.on(:create_bid_successful) { |bid_id| redirect_to bid_path(bid_id) }
	  create_bid.on(:create_bid_failed)     { |bid_id| render action: :new }
	  
	  create_bid.call
	end

	private

	def new_form
	  Bids::Create::Form.new(params[:form])
	end
end

This is the context which bring each part together. I’ve not included the view the user sees but you can imagine the use of form_for to show a HTML form which the user can interact with.

We have nice seperation of concerns and it wasn’t too much work.

Now we want to send an email to the seller to notify them of the new bid using ActionMailer.

We could put the mailing code directly in the service object. But what, if in the future, we want to create bid without sending an email.

I see email as a UI and not part of the core business logic, its on the outside of the Hexagon. So instead, in the controller, we can subscribe a listener to our service object which will react to the create_bid_successsful event:

create_bid.subscribe(Orders::Notifier.new, prefix: 'on')

Listener

class Orders::Notifier
  def on_create_bid_successful(bid_id)
    bid = Bid.find(bid_id)
    
    Mailer.bid_created(bid).deliver
  end
end

and Mailer

class Orders::Notifier::Mailer < ActionMailer
  # the usual...
end

PART 2

Now the seller will be notified when a bid is placed on their auction.

This is great and we can deploy this feature to staging for testing. It gets merged in to production and deployed. Time passes and we do the usual stuff to cope with demand; cache as much as possible, add web servers, add database slaves, increase the VM size etc. But the bottle neck is in the Rails app itself.

The first thing we can do is put any work which does not need to happen within the web request on to a background queue.

The first easy win is to send the email asyncrounsly, this gives us two wins: 1.) placing a bid is more responsive 2.) we can use a background queue which can live on another server and have its own resources which do not impact on the web server. The queue can also be scaled independantly of the web app.

To do this we only need to make a few changes:

If your using Rails 4 you can use Activejob. If you are using anything else then Sidekiq and Cellulioid are avalible.

Firstly we add wisper-activejob to our Gemfile. This allows us to add the sync: true option when subscribing listeners to have the events handled async. The controller action remains the same expect we add sync: true when subscribing the Notifier:

create_bid.subscribe(Orders::Notifier.new, prefix: 'on', async: true)

Excellent, a minor code change and we have async mail sending. Note that thanks to ActiveJob and ActionMailer new async features we could have done this a different way, but note this will work for pre-Rails 4 applications as well. Its important not to focus too much on the actual example of sending email but the idea behind it. It could equally be recording of statistics or creating an audit trail which happens in a listener.

3 years later…

PART 3

Our startup is doing well we have millions of bids per day. The server which runs sidekiq is struggling despite being on dedicated hardware. There are just too many background jobs to be processed. We need a way to scale different types of jobs independantly.

Here we can turn to a SOA archecture. Let imagine the heaviest burden on Sidekiq is sending of emails for bids, if we could break that off we could accomodate more traffic.

First of all we need to deploy RouteMaster, a REST based Event Bus. Our Rails app needs to be able to push events to Routemaster and a seperate service needs to subscribe to those events. The Rails app then provides an end point from which the service can fetch the entity relating to the event.

Rails app -publish event-> RouteMaster -push event-> Subscribed Service <-fetch entity- Rails app

gem 'routemaster-client'

Instead of subscribing the notifier we are going to subscribe a generic listener which will simply push the event on to RouteMaster.

class RouteMasterListener
  def on_create_bid_successful(bid_id)
    client.created(:bid, "https://my-webapp.com/bids/#{bid_id}")
  end
  
  private
  
  def client
  
  end
end

Using the RouteMaster client we publish an event to RouteMaster which will route the event to the subscribed listener which are running in different processes.

So now we need a listening service which will handle the event. It needs a HTTP endpoint for receive the events. For this we could use Grape, Lotus, Sinatra or Rails.

In the apps initalizer we need to subscribe to the endpoint for created bids.

client.subscribe...

And now for the endpoint which will receive new events. Here we have an important choice, we could include an activerecord Bid model which queries the same database as the Rails app or we could expose a JSON endpoint on the Rails app which the service can query. If we share the database this will create a coupling. Changes to the model will need to be sycronised.

Events endpoint

class BidsController < ApplicationController
  def create
    bid_id = params[:...] <- get from JSON event
    
    # use faraday or hyperclient to fetch bid from Rails app
    # and send the email.
  end
end

A better option is to use routemaster-drain which provides us with a rack mountable endpoint for routemaster events to which we can subscribe listeners.

require 'routemaster/drain/basic'
$app = Routemaster::Drain::Basic.new
$app.on(:events_received) do |batch|
  batch.each do |event|

     if event.type == 'create'
      # event created...
      json = Faraday.fetch(event.url)
      bid = Bid.new(json)
      Mailer....
      
    end
  end
end

Because we only care about one event and reacting to it in one way I am using $app.on to register a block for the events_received event.

You can also use $app.subscribe to subscribe objects.

In routes.rb we mount the endpoint

#...

routemaster-drain uses Wisper to allow us to subscribe listneners which will receive batches of events.

$app.subscribe(EventHandler.new, prefix: 'on')

Events handler

class EventHandler
  def on_events_received(batch)
    batch.each do |event|
       end
  end
end

However now we don’t have access to the Bid model or the database (we wouldn’t want to couple to the database anyway). Therefore we need to query the web app via a RESTful HTTP request.

class Bid
  def self.find(bid_id)
    # do REST call to Rails app...
  end
  
  def self.url_for(bid_id)
    "http://bidsapp.com/bids/#{bid_id}"
  end
end

The beauty of duck typing, our listener does not change.

If you needed to fetch Bids from other web services you would proberbly want to split Bid off in to its own gem, bids-client.

What is important is we use hypermedia, i.e. we refer to entities by a URL. This allows us to include links to other resources without the client needing to know where they reside before hand.

The mailer class is moved from to the original Rails app in to our “bids web service” since this is where we will send our email from.

Now back to our original Rails app we need to add an endpoint which will return the bid as JSON.

class BidsController
  def show
    bid = Bid.find(bid_id)
    # use some gem which can create a json response.
  end
end

This might return something like:

_links: 
  buyer { url: https://webapp.com/users/3 }

Now with this setup we can start to scale. RouteMaster provides some really nice tools.

What we might end up doing is breaking off the entire “bids” concept in to the new service, so the Rails app talks to the service instead of the database. So in essence everything under the Bids namespace can be serviceized, this is another great reason for namespacing in this way. In DDD this might be what you would consider a bounded content. We end up with bids-server and bids-client. This would move us closer to Fine gained, RESTful Hypermedia.

As you can see the main point is we can do this in stages and make gains along the way, there is no big rewrite. But a gradual move towards a SOA archecture.

I am only really touching the surface of SOA here, but this gives us a path towards it. You shouldn’t tart with SOA but if success happens it helps to have a path without too much resistance.