Developer docs
Auto Light Dark
Auto Light Dark

External Service Registration Guide with Breakers

Last Updated: October 20, 2026

This doc explains when a service needs to be registered with Breakers (vets-api’s circuit-breaker library), how to wire it up, and what the require situation actually depends on.

What Breakers does, briefly

Breakers wraps outbound HTTP calls to a named "service" and tracks their success/failure rate in Redis. If a service's failure rate crosses a threshold, Breakers trips the circuit and fails fast on subsequent calls instead of hammering an unhealthy upstream. It also feeds StatsD/Datadog metrics per service name, so any properly-registered client gets uptime dashboards and alerting included. See the doc Configure breakers metrics in Vets API for external services for more details.

When to add a service

Add a service to the services array in config/initializers/breakers.rb when all of the following are true:

  1. It makes outbound HTTP calls to an external or upstream system — a Faraday-based client, essentially. Breakers only wraps HTTP; it has nothing to offer a service with no network calls.

  2. Failures in that system should count against a circuit — i.e., repeated failures should eventually short-circuit new requests rather than let every caller hang or retry against a downed dependency. If a dependency is purely fire-and-forget, low-volume, or already has its own retry/backoff strategy that Breakers would just duplicate, it may not be worth wrapping.

  3. You want it to show up in the shared Breakers dashboard/alerting, not just its own bespoke logging. Registering it is what makes it visible alongside every other upstream service in Datadog (link requires Datadog access).

Skip registration for:

  • Abstract base configuration classes. Several classes in the initializer are deliberately not registered because they're parents that concrete subclasses inherit from (e.g. EVSS::Configuration, VAProfile::Configuration, BEP::Configuration). Registering the base and every subclass would double-count or conflict. Register the concrete subclass only.

  • Classes that inherit their parent's breakers config without overriding it.EVSS::DisabilityCompensationForm::Dvp::Configuration and GI::LCPE::Configuration for example–they ride on the parent's registration.

  • Anything not making real outbound calls in the affected code path.

If you're unsure whether a given client should be registered, the fastest check is: does calling .breakers_service (or .instance.breakers_service) on it already exist, or would you be adding it from scratch? If it's genuinely new, ask in the owning team's channel before assuming — there may be a reason it was left out (see the commented-out lines in the initializer, e.g. # Ibm::Configuration.instance.breakers_service, each of which should have a reason.

How to wire it up

There are two established patterns in the codebase. Use whichever matches how the rest of the client is structured. Don't introduce a third pattern.

Pattern A: Configuration singleton (most common)

Most existing entries follow this shape: the HTTP client has a companion Configuration class (typically inheriting from Common::Client::Configuration::REST or similar), and Configuration defines breakers_service as an instance method, with Configuration.instance as the singleton accessor.

Ruby
# lib/some_service/configuration.rb
module SomeService
  class Configuration < Common::Client::Configuration::REST
    def breakers_service
      Breakers::Service.new(
        name: 'SomeService',
        request_matcher: proc do |breakers_service, _request_env, request_service_name|
          request_service_name == breakers_service.name
        end
      )
    end
  end
end

Registered as: SomeService::Configuration.instance.breakers_service

Pattern B: Bare class method on the client itself

A few clients (e.g. EVSS::ClaimsService, ClaimsApi::LocalBGS, and Idp::Client) don't have a separate Configuration class — the client class defines self.breakers_service directly.

Ruby
module Idp
  class Client
    SERVICE_NAME = 'IDP'

    def self.breakers_service
      Breakers::Service.new(
        name: SERVICE_NAME,
        request_matcher: proc do |breakers_service, _request_env, request_service_name|
          request_service_name == breakers_service.name
        end
      )
    end
  end
end

Registered as: Idp::Client.breakers_service

Use Pattern B only when there's genuinely no Configuration class to hang it off of. If you're building a new client from scratch and it's non-trivial (multiple endpoints, etc.), prefer introducing a Configuration class and following Pattern A, it's the majority convention and keeps future maintainers from having to remember two shapes.

The request_matcher is boilerplate, not optional

Every entry in the initializer uses the same request_matcher proc — comparing the tagged request_service_name against the service's own name. Copy it verbatim; there's no reason to deviate unless you have a specific multi-tenant or shared-connection scenario Breakers needs to disambiguate differently.

Wire the middleware into the Faraday connection

Registering the service in the initializer only teaches Breakers the service exists — it does nothing unless the client's Faraday connection actually uses the :breakers middleware, and the service_name: passed there must match the registered name exactly (string equality, so watch for typos/casing):

Ruby
Faraday.new(url: base_url) do |conn|
  conn.use(:breakers, service_name: SERVICE_NAME)  # must come before conn.response :raise_error
  conn.request :json
  conn.response :json
  conn.response :raise_error   # breakers needs to see the exception this raises
  conn.adapter :net_http
end

Ordering matters: :breakers should be added before :raise_error in the builder stack so it wraps (and can observe failures from) everything below it. In Faraday's builder, middlewares added earlier become outer wrappers, so this ordering makes :breakers see exceptions raised deeper in the stack.

Do I need to require my config in breakers.rb?

It depends! This trips people up because the initializer’s require list is inconsistent-looking at a glance, but there's a reason behind it.

BLUF: if your new service's config class lives in app/, skip the require. If it lives in lib/, add require 'path/to/file' at the top of the initializer, grouped with the others.

Details:

  • Anything under app/** (including app/services/**) is autoloaded by Zeitwerk by Rails convention. Referencing Idp::Client inside the initializer's to_prepare block is enough — Zeitwerk resolves it on first reference. No explicit require is needed, and none should be added; it'd be redundant.

  • Anything under lib/** is not guaranteed to be on the autoload path in every environment/config (depends on config.autoload_paths / config.eager_load_paths setup for lib). That's why the initializer has an explicit require 'some_service/configuration' for every lib-based Configuration class it references. Without it, referencing the constant could raise NameError depending on load order and environment.

  • The block starting with: # --- Added by breakers audit (lib/**/configuration.rb candidates) --- exists specifically because those were all lib/ classes swept up in a Platform audit. The comment is flagging that these needed explicit requires because they're in lib, unlike the ones above that line which may already have been app/-based or otherwise already loaded elsewhere.

Checklist for adding a new service

  1. Confirm the service makes real outbound HTTP calls and failures should count against a circuit (see "When to add a service" above).

  2. Add breakers_service to the client or its Configuration class, using the standard request_matcher boilerplate.

  3. Wire conn.use(:breakers, service_name: ...) into the Faraday builder, before conn.response :raise_error.

    1. Make sure the name of your service is unique among all services in the repo.

  4. Add your service to config/initializers/breakers.rb following the guidance in the section, “Do I need to require my config in breakers.rb?”

    1. Add the entry to the services array in the initializer.

  5. Add a spec asserting breakers_service returns a properly-named Breakers::Service and that the connection's builder includes Breakers::UptimeMiddleware first, with the right service_name.

  6. If you're deliberately not registering a related/adjacent class, add a one-line comment next to the NOTE: block at the top of the services array explaining why.

Example: registering a new external service

This walks through adding breakers support for a hypothetical new integration, Widget, using Pattern A (the majority convention — a Configuration class separate from the client). All three files live in lib/widget/, so note the require at the bottom, per the rule above.

1. lib/widget/configuration.rb

Ruby
# frozen_string_literal: true

module Widget
  class Configuration < Common::Client::Configuration::REST
    SERVICE_NAME = 'Widget'
    self.read_timeout = Widget::Configuration::SETTINGS.timeout || 30

    def base_path
      Settings.widget.url
    end

    def service_name
      SERVICE_NAME
    end

    def breakers_service
      Breakers::Service.new(
        name: SERVICE_NAME,
        request_matcher: proc do |breakers_service, _request_env, request_service_name|
          request_service_name == breakers_service.name
        end
      )
    end

    def connection
      @connection ||= Faraday.new(base_path, headers: base_request_headers, request: request_options) do |conn|
        conn.use(:breakers, service_name: SERVICE_NAME)
        conn.request :json
        conn.response :betamocks if mock_enabled?
        conn.response :json, content_type: /\bjson$/
        conn.response :raise_error
        conn.adapter Faraday.default_adapter
      end
    end

    def mock_enabled?
      Settings.widget.mock || false
    end
  end
end

2. lib/widget/service.rb

The service/client class handles the actual API calls, delegating connection details to Configuration.

Ruby
# frozen_string_literal: true

module Widget
  class Service < Common::Client::Base
    include Common::Client::Concerns::Monitoring
    configuration Widget::Configuration

    STATSD_KEY_PREFIX = 'api.widget'

    def get_widget(id)
      with_monitoring do
        perform(:get, "widgets/#{id}", nil, request_headers)
      end
    end

    def create_widget(payload)
      with_monitoring do
        perform(:post, 'widgets', payload.to_json, request_headers)
      end
    end

    private

    def request_headers
      { 'Content-Type' => 'application/json' }
    end
  end
end

(Common::Client::Base's perform already routes through configuration.connection, so the breakers middleware configured in step 1 applies automatically to every call this service makes.)

3. config/initializers/breakers.rb

Add the require (since this lives in lib/) and the registration entry:

Ruby
# top of file, alongside the other lib/**/configuration.rb requires
require 'widget/configuration'

Ruby
services = [
  Idp::Client.breakers_service,
  Widget::Configuration.instance.breakers_service,
  DebtManagementCenter::DebtsConfiguration.instance.breakers_service,
  # ...
]

4. spec/lib/widget/configuration_spec.rb

Ruby
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Widget::Configuration do
  describe '#breakers_service' do
    it 'returns a Breakers::Service named Widget' do
      service = described_class.instance.breakers_service

      expect(service).to be_a(Breakers::Service)
      expect(service.name).to eq(Widget::Configuration::SERVICE_NAME)
    end

    it 'matches requests by the Widget service_name' do
      service = described_class.instance.breakers_service

      expect(service.handles_request?(request_env: nil, service_name: 'Widget')).to be(true)
      expect(service.handles_request?(request_env: nil, service_name: 'OTHER')).to be(false)
    end
  end

  describe '#connection' do
    subject(:connection) { described_class.instance.connection }

    it 'includes breakers middleware first with the Widget service_name' do
      expect(connection.builder.handlers.first).to eq(Breakers::UptimeMiddleware)
      expect(connection.app).to be_a(Breakers::UptimeMiddleware)
      expect(connection.app.service_name).to eq(Widget::Configuration::SERVICE_NAME)
    end
  end
end

This mirrors the checklist above: Configuration owns breakers_service and the Faraday wiring (step 1), Service is a thin client that inherits the connection (step 2), the initializer gets a require plus a registration line since this is a lib/ class (step 3), and the spec asserts both the service naming and the middleware ordering (step 4).


Help and feedback