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:
-
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.
-
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.
-
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::ConfigurationandGI::LCPE::Configurationfor 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.
# 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.
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):
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/**(includingapp/services/**) is autoloaded by Zeitwerk by Rails convention. ReferencingIdp::Clientinside the initializer'sto_prepareblock is enough — Zeitwerk resolves it on first reference. No explicitrequireis 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 onconfig.autoload_paths/config.eager_load_pathssetup forlib). That's why the initializer has an explicitrequire 'some_service/configuration'for everylib-basedConfigurationclass it references. Without it, referencing the constant could raiseNameErrordepending on load order and environment. -
The block starting with:
# --- Added by breakers audit (lib/**/configuration.rb candidates) ---exists specifically because those were alllib/classes swept up in a Platform audit. The comment is flagging that these needed explicit requires because they're inlib, unlike the ones above that line which may already have beenapp/-based or otherwise already loaded elsewhere.
Checklist for adding a new service
-
Confirm the service makes real outbound HTTP calls and failures should count against a circuit (see "When to add a service" above).
-
Add
breakers_serviceto the client or itsConfigurationclass, using the standardrequest_matcherboilerplate. -
Wire
conn.use(:breakers, service_name: ...)into the Faraday builder, beforeconn.response :raise_error.-
Make sure the name of your service is unique among all services in the repo.
-
-
Add your service to
config/initializers/breakers.rbfollowing the guidance in the section, “Do I need torequiremy config inbreakers.rb?”-
Add the entry to the
servicesarray in the initializer.
-
-
Add a spec asserting
breakers_servicereturns a properly-namedBreakers::Serviceand that the connection's builder includesBreakers::UptimeMiddlewarefirst, with the rightservice_name. -
If you're deliberately not registering a related/adjacent class, add a one-line comment next to the
NOTE:block at the top of theservicesarray 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
# 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.
# 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:
# top of file, alongside the other lib/**/configuration.rb requires
require 'widget/configuration'
services = [
Idp::Client.breakers_service,
Widget::Configuration.instance.breakers_service,
DebtManagementCenter::DebtsConfiguration.instance.breakers_service,
# ...
]
4. spec/lib/widget/configuration_spec.rb
# 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
-
Get help from the Platform Support Team in Slack.
-
Submit a feature idea to the Platform.