/ Rails

Organizing External Services in Rails

Our applications rarely work in isolation. Gone are the days where your app is just talking to a single database and storing files to local disc. Nowadays, apps talk to many external services like Sentry, Stripe, Twitter, etc.

External services are often added one by one in various places in a system, and it is easy to lose track of them. Because of this, at Product Hunt we have strict rules for dealing with external services. This blogs post shows and explains those rules.

Our main goals are to isolate those external services and to make it easy for team members to know why and how services are used.

1. Keep all services in a single place

All external services live in the app/services/external folder and are namespaced under the External module. All network-related code goes there. This includes calls to microservices we wrote ourselves.

In this way, we can see most of the outcoming network calls from our application just by opening this folder.

2. Wrap the external services in a facade module

All external services are accessed via External::[Service]Api facade module.

The following is our template for such a module.

# frozen_string_literal: true

# Documentation
#
# - service: [url to service]
# - manage: [url to manage tokens]
# - api: [url to api documentation]
# - gem: [gem we are using (optional)]
# [- ... other links]

module External::[Service]Api
  extend self

  # documentation: [documentation]
  def perform_action(args)
    # use HTTParty or gem for this external service
  end
end

Usually, the API facades are just a bag of module methods.

Here is an example from our External::UnsplashApi:

# frozen_string_literal: true

# Documentation
#
# - service: https://unsplash.com/
# - api: https://unsplash.com/documentation
# - gem: https://github.com/unsplash/unsplash_rb
# - portal: https://unsplash.com/oauth/applications

module External::UnsplashApi
  extend self

  def search(query)
    Unsplash::Photo.search(query, 1, 12, 'landscape')
  end

  def track_download(id)
    photo = Unsplash::Photo.find(id)
    photo.track_download
  rescue Unsplash::NotFoundError
    nil
  end
end

Even when the external service has a gem to access it, the gem is never used outside this module.

If the gem returns an instance of a response object, we try to wrap it with a class defined by us. We started wrapping those response objects quite late.

We expose only what we use from the services.

3. Storing credentials

Rails added support for encrypted credentials. Our newest applications use this mechanism for storing external services secrets.

We use the following commands to work with credentials.

EDITOR=vim rails credentials:edit --environment development
EDITOR=vim rails credentials:edit --environment production

Credentials are stored in YAML. We always include a link to where a certain credential was taken because it is very irritating to search where token and secret was taken from—especially when dealing with Google APIs.

# Taken from: [url to where those credentials are taken]
[sevice_name]_token: [...]
[sevice_name]_secret: [...]

Conclusion

We are structuring our external services for the following reasons:

  • Know precisely which services are external to the system
  • Make it evident that we are interacting with an external system where network calls are involved
  • Have an obvious surface area of external service, where we know what exact features we are using from them
  • Having a facade makes it easier to add proper logging, caching, and error handling
  • Simpler upgrade paths of APIs -- if an external gem has a breaking change, it only affects the facade object