Let me tell you a story: Once upon a time there was a rails app. It was a good rails app, like all rails new apps. But over time a darkenss started shrouding the land. The app grew. Models started piling up. The developers pushed the models deep into the dungeons of namespaces. And there they sat, in the darkness, quetly growing old and crusty with unsupported code.

But one day, there was great upheaval. The knights of SOA have arrived! They took those models out of the dungeons and cleaned them. They wrapped these models in new rails apps, smaller and shinier, with APIs to protect the users from the raw power of the models. The old app was rebuilt on top of the APIs with clearly defined boundaries. And everyone lived happily ever after.


Or something like that. Let's talk about what SOA means in the Rails ecosystem. Or, rather, what it means in the Avvo ecosystem. Over time, as our app grew, we decided that it would be best to cut it up into services. This will be a brief overview of our stack.

Our main app is still a rails app and our services are gently modified rails-api apps. They communicate through JSON. The client uses the JsonApiClient gem to formulate the requests and parse the responses.

JsonApiClient

JsonApiClient is a neato little gem that Jeff Ching wrote. It makes it easy for the client to never have to worry about paths or request parsing. All of that is done by the gem.

Modified rails-api

Our implementation, while unfortunately not public, is a thin wrapper around rails-api that formats the responses as:

  {
    # ActiveModel::Serializer serialized array of objects
    meta: {
      status: HTTP status,
      page: current page,
      per_page: count of entries per response page,
      total_pages: toal number of pages with the above per_page count,
      total_entries: total number of records
    }
  }

Example

It's easier to explain how this works with an example. Let's pretend that this blog is a rails app that has an API backend and examine how we would render the first page.

How we talk to one another

The blog makes a call to the blagablag API to ask for all the posts that may be available (or, really, the first page). Let's look at what the code would look like.

Main blog app

Controller

The controller looks much like any other controller.

class BlogController
  def index
    # first page of results.
    @posts = Blagablag::Post.order(:updated_at).to_a
  end

  def show
    @post = Blagablag::Post.find(params[:id])
  end
end

With the exception that you're now asking the Blagablag::Post class for info, which is a JsonApiClient class.

Client

module Blagablag
  class Post < JsonApiClient::Resource
    # this would normally be in a base class
    self.site = "blagablag.avvo.com/api/1"
  end
end

Sure is empty in there, huh? That's because JsonApiClient::Resource is handling all of the routing for us. It knows how to build all the standard CRUD routes, so all you have to do is call them.

Note: It's possible (and easy) to build custom routes, but you have to define those in the client. RTM for more details.

blagablag API

The API side is a bit more involved (for you, because we do not currently have an open source implementation of the server), but a simplified version would look something like:

module Blagablag
  module V1
    class PostsController < ActionController::API
      # main controller to handle all where requests
      def index
        @posts = Post.scope.where(params)
        @posts = @posts.order(params[:order]) if params[:order]
        # process page params
        @posts = paginate(@posts)

        render json: format(@posts)
      end

      # where client.find(id) goes
      def show
        @post = Post.find(params[:id])

        render json: format(@post)
      end

      private

      # this is all greatly simplified for the example

      def format(objects)
        {
          posts: Array(objects),
          meta:
            status: 200,
            page: @page,
            per_page: @per_page,
            total_pages: @total/@per_page,
            total_entries: @total
          }
        }
      end
    end
  end
end

And that's pretty much it. There's a little bit of magic that we do with our servers that makes some of this a bit cleaner, but that is our stack. We're still learning how to manage all the servers and interdependencies, but this has sped up our system dramatically and has forced us to really think about system design in a way that our old monolith never could.