@brandur

  • Explain Heroku

Post-Rails? Composable Applications with a First-class API

A Study of
Composition

Heroku's API Team

We were also in charge of this.

  • Our "monorail"
  • Wrap our heads around the entire app

Meet Core

  • A Rails app
  • Then, a BIG RAILS APP
  • Severe impact on development velocity and effort

We broke things out.

BUTC

Break up the core

  • Many companies run into this problem, one is our parent Salesforce

Still too big

Introducing Dashboard

Dashboard Implementation

API calls happen on the backend.

class AppsController < ApplicationController
  def index
    @apps = @api.get_apps
  end
end

Where's the fat client?

Metrics

  • Dashboard
    • +4500 LOCs (3200 Ruby + 1300 templates)
  • Core
    • 66k LOCs (61k Ruby + 5k templates)
    • Down to 55k LOCs (55k Ruby)
    • -11k LOCs (~15%)

Not the final win,
but one we'll take gladly.

=

+

Our API is now an API.

  • The responds_to block is no longer needed.

Heroku Manager

Deeper Composition

Manager Web
Manager API
Core API
  • Manager is further composed into two tiers
    • API
    • Web
  • Manager's API layer injects additional business logic built on underlying primitives: teams & organizations

A good API is a reusable API

Dashboard
CLI
Manager
Core API
  • Core's API separately consumed by Dashboard, Manager, and the CLI

App
Composition

Composability

Web
Our web users

API
CLI, Dashboard, Manager API + developers

Internal Services

Service Oriented Architecture (SOA)

Not a new idea.

Service-Oriented Architecture: Concepts, Technology, and Design

*2005

  • Eight principles from industry
  • Published by Thomas Erl

First-class APIs

Rails as frontend

Rails as API

rails-api as API

gem install rails-api
rails-api new facts_api

Sinatra as API

get "/facts/:id" do |id|
  fact = Fact.first(id: id.to_i)
  [200, encode_json(fact)]
end

post "/facts" do
  fact = Fact.new(fact_params)
  fact.save
  [201, encode_json(fact)]
end

Grape as API

class Facts::API < Grape::API
  version 'v0', using: :header

  resources :facts do
    get ":id" do
      fact = Fact.first(id: params[:id].to_i)
      encode_json(fact)
    end

    post do
      fact = Fact.create(fact_params)
      encode_json(fact)
    end
  end
end

Organization

  • In many cases where a service was broken off, it was done by someone who cared about that portion, and that team member would go with it
  • Smaller and more specialized teams
    • Before: three backend people, a designer, and a handful of frontend engineers

      ⚥ ⚥ ⚥ ⚥ 

    • After: API is three backend people; Dashboard is a designer and a frontend engineer

      ⚥ ⚥ ⚥ ↔ ⚥ 

Happiness

And the best part?

Backend people
don't

have to think
about CSS floats

Flexibility

Lessons Learn(t/ing)

Stubs

We started with this.

But we don't recommend it.

class AppsController < ApplicationController
  def index
    @apps = if production?
      @api.get_apps
    else
      []
    end
  end
end

Artifice

stub(Config).billing_api { "https://billing-api.localhost" }
stub(Config).process_api { "https://process-api.localhost" }

# stub with fully functional Rack apps
Artifice::Excon.activate_for(Config.billing_api,
  BillingAPIStub.new)
Artifice::Excon.activate_for(Config.process_api,
  ProcessAPIStub.new)

And you get Rack apps!

web:         thin start -R config.ru -p $PORT
billing_api: thin start -R stubs/billing_api.ru -p $PORT
process_api: thin start -R stubs/process_api.ru -p $PORT

These apps are platform deployable.

Teams should own their own API stubs.

  • This is one we're still thinking about
  • API change --> you could run against all known internal test suites
  • We're also thinking about how smart the stubs should be? A very dumb version of the service they're stubbing or the most basic stub possible.

Platform

I know somebody that does all this.

@brandur

brandur@mutelight.org