Railsmith
Railsmith is a service-layer gem for Rails. It standardizes domain-oriented service boundaries with sensible defaults for CRUD operations, bulk operations, result handling, and cross-domain enforcement.
Requirements: Ruby >= 3.1.0, Rails 7.0–8.x
Installation
# Gemfile
gem "railsmith"bundle install
rails generate railsmith:installThe install generator creates config/initializers/railsmith.rb and the app/services/ directory tree.
Quick Start
Generate a service for a model:
rails generate railsmith:model_service UserCall it:
result = UserService.call(
action: :create,
params: { attributes: { name: "Alice", email: "alice@example.com" } }
)
if result.success?
puts result.value.id
else
puts result.error.message # => "Validation failed"
puts result.error.details # => { errors: { email: ["is invalid"] } }
endSee docs/quickstart.md for a full walkthrough.
Result Contract
Every service call returns a Railsmith::Result. You never rescue exceptions from service calls.
# Success
result = Railsmith::Result.success(value: { id: 123 }, meta: { request_id: "abc" })
result.success? # => true
result.value # => { id: 123 }
result.meta # => { request_id: "abc" }
result.to_h # => { success: true, value: { id: 123 }, meta: { request_id: "abc" } }
# Failure
error = Railsmith::Errors.not_found(message: "User not found", details: { model: "User", id: 1 })
result = Railsmith::Result.failure(error:)
result.failure? # => true
result.code # => "not_found"
result.error.to_h # => { code: "not_found", message: "User not found", details: { ... } }Generators
| Command | Output |
|---|---|
rails g railsmith:install |
Initializer + service directories |
rails g railsmith:domain Billing |
app/domains/billing.rb + subdirectories |
rails g railsmith:model_service User |
app/services/user_service.rb |
rails g railsmith:model_service Billing::Invoice --domain=Billing |
app/domains/billing/services/invoice_service.rb |
rails g railsmith:operation Billing::Invoices::Create |
app/domains/billing/invoices/create.rb |
CRUD Actions
Services that declare a model inherit create, update, destroy, find, and list with automatic exception mapping:
class UserService < Railsmith::BaseService
model(User)
end
# create (context: is optional from 1.1 onward)
UserService.call(action: :create, params: { attributes: { email: "a@b.com" } })
# update
UserService.call(action: :update, params: { id: 1, attributes: { email: "new@b.com" } })
# destroy
UserService.call(action: :destroy, params: { id: 1 })Common ActiveRecord exceptions (RecordNotFound, RecordInvalid, RecordNotUnique) are caught and converted to structured failure results automatically.
Bulk Operations
# bulk_create
UserService.call(
action: :bulk_create,
params: {
items: [{ name: "Alice", email: "a@b.com" }, { name: "Bob", email: "b@b.com" }],
transaction_mode: :best_effort # or :all_or_nothing
}
)
# bulk_update
UserService.call(
action: :bulk_update,
params: { items: [{ id: 1, attributes: { name: "Alice Smith" } }] }
)
# bulk_destroy
UserService.call(
action: :bulk_destroy,
params: { items: [1, 2, 3] }
)All bulk results include a summary (total, success_count, failure_count, all_succeeded) and per-item detail. See docs/cookbook.md for the full result shape.
Domain Boundaries
Tag services with a bounded context and track it through all calls:
rails generate railsmith:domain Billing
rails generate railsmith:model_service Billing::Invoice --domain=Billingmodule Billing
module Services
class InvoiceService < Railsmith::BaseService
model(Billing::Invoice)
domain :billing
end
end
endPass context when you need domain or tracing data (context: is optional; omit it to use thread-local Context.current or an auto-built context):
ctx = Railsmith::Context.new(domain: :billing, request_id: "req-abc")
Billing::Services::InvoiceService.call(action: :create, params: { ... }, context: ctx)When the context domain differs from a service's declared domain, Railsmith emits a cross_domain.warning.railsmith instrumentation event.
Configure enforcement in config/initializers/railsmith.rb:
Railsmith.configure do |config|
config.warn_on_cross_domain_calls = true # default
config.strict_mode = false
config.on_cross_domain_violation = ->(payload) { ... }
config.cross_domain_allowlist = [{ from: :catalog, to: :billing }]
endError Types
| Code | Factory |
|---|---|
validation_error |
Railsmith::Errors.validation_error(message:, details:) |
not_found |
Railsmith::Errors.not_found(message:, details:) |
conflict |
Railsmith::Errors.conflict(message:, details:) |
unauthorized |
Railsmith::Errors.unauthorized(message:, details:) |
unexpected |
Railsmith::Errors.unexpected(message:, details:) |
Architecture Checks
Detect controllers that access models directly (and related service-layer rules). From the shell:
rake railsmith:arch_check
RAILSMITH_FORMAT=json rake railsmith:arch_check
RAILSMITH_FAIL_ON_ARCH_VIOLATIONS=true rake railsmith:arch_checkFrom Ruby (same environment variables and exit codes as the task), after require "railsmith/arch_checks":
Railsmith::ArchChecks::Cli.run # => 0 or 1See Migration for optional env:, output:, and warn_proc: arguments.
Documentation
- Quickstart — install, generate, first call
- Cookbook — CRUD, bulk, domain context, error mapping, observability
- Legacy Adoption Guide — incremental migration strategy
- Migration — upgrading from 1.0.x to 1.1.x (and earlier releases)
- Changelog
Development
bin/setup # install dependencies
bundle exec rake spec # run tests
bin/console # interactive promptCI runs the suite against Rails 7 and Rails 8 using gemfiles/rails_7.gemfile and gemfiles/rails_8.gemfile (Ruby 3.1–3.3; Rails 8 is not paired with Ruby 3.1 in CI). To reproduce a matrix cell locally:
BUNDLE_GEMFILE=gemfiles/rails_7.gemfile bundle install
BUNDLE_GEMFILE=gemfiles/rails_7.gemfile bundle exec rspecTo install locally: bundle exec rake install.
Releasing
With lib/railsmith/version.rb and CHANGELOG.md updated and committed, run bundle exec rake release to tag v + version, build the gem, and push to RubyGems (requires gem push credentials and a clean git state). To publish manually: gem build railsmith.gemspec then gem push railsmith-X.Y.Z.gem.
Contributing
Bug reports and pull requests are welcome at github.com/samaswin/railsmith.