RailsArchitect
A comprehensive architectural enforcement and code quality tool for Rails applications.
Rails Architect helps maintain clean Rails architecture by automatically enforcing software design principles and detecting common anti-patterns.
Features
- SOLID Principles: Enforces Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion principles
- KISS Principle: Detects overly complex code and suggests simplification
- DRY Principle: Identifies code duplication and repeated patterns
- Automated Checks: Run as rake tasks or integrate into CI/CD pipelines
SOLID Principles Guide
Here's a quick, Rails-flavored guide to the SOLID principles—with tiny examples and common pitfalls.
S — Single Responsibility
One class = one reason to change. Rails gotchas: "Fat models," callback soup, controllers doing business logic.
Smell
class Order < ApplicationRecord
after_create :send_receipt_email
def total_cents; ... complex tax/discount logic ... end
end
Refactor
# app/services/orders/calculate_total.rb
class Orders::CalculateTotal
def self.call(order) = ... # pure calc
end
# app/jobs/order_receipt_job.rb
class OrderReceiptJob < ApplicationJob
def perform(order_id) = OrderMailer.receipt(Order.find(order_id)).deliver_now
end
# app/models/order.rb
class Order < ApplicationRecord
def total_cents = Orders::CalculateTotal.call(self)
end
Tip: prefer service objects, form objects, query objects, mailer jobs over callbacks.
O — Open/Closed
Open for extension, closed for modification. Use composition & small objects; avoid editing core classes for each new case.
Example: pluggable pricing rules
class PriceCalculator
def initialize(rules = [LoyaltyRule.new, CouponRule.new])
@rules = rules
end
def call(order)
@rules.reduce(order.subtotal) { |total, rule| rule.apply(order, total) }
end
end
Add a new rule class, don't edit PriceCalculator.
L — Liskov Substitution
Subtypes must be usable wherever their base is. Rails gotcha: STI models or polymorphic interfaces that don't honor contracts.
Bad
class Payment < ApplicationRecord; end
class CreditCardPayment < Payment; def refund!; ...; end
class GiftCardPayment < Payment; end # no refund!
Better (explicit interface)
module Refundable
def refund! = raise NotImplementedError
end
class CreditCardPayment < ApplicationRecord
include Refundable
def refund!; ... end
end
class GiftCardPayment < ApplicationRecord
include Refundable
def refund!; ... end
end
Or avoid STI; use separate models + shared interface modules and tests.
I — Interface Segregation
Clients shouldn't depend on methods they don't use. Rails tip: keep "narrow" objects per use-case: FormObject, Presenter, Query—not one god-service.
Example
# Too broad:
class AccountService; def create; def suspend; def invite; def export_csv; end; end
# Split:
class Accounts::Creator; end
class Accounts::Suspender; end
class Accounts::Inviter; end
For APIs, expose slim endpoints; for views, use ViewComponents/Presenters instead of fat helpers.
D — Dependency Inversion
Depend on abstractions, not concretions. Rails tip: inject collaborators; don't hard-wire globals (e.g., a specific HTTP client).
Example
# app/services/notifications/send_sms.rb
class Notifications::SendSms
def initialize(client: Sms::TwilioClient.new) = @client = client
def call(to:, body:) = @client.deliver(to:, body:)
end
# test
fake = Class.new { def deliver(...) = true }.new
expect(Notifications::SendSms.new(client: fake).call(to: "...", body: "...")).to be true
Common Rails patterns that support SOLID
- Service objects (
app/services/...) for business actions. - Form objects (e.g.,
Reform, or POROs withActiveModel::Model) to keep controllers skinny. - Query objects for complex scopes (compose instead of stuffing models).
- Policies (
Pundit) for authorization logic (SRP). - ViewComponent (or presenters) to isolate view behavior (ISP/SRP).
- Background jobs for side effects (mail, webhooks) instead of callbacks.
- Adapters/ports for external services (DIP).
Anti-patterns to watch
- Heavy
ActiveSupport::Concernmixing multiple responsibilities. - Model callbacks triggering network calls (violates SRP & DIP).
- STI where subclasses violate the base contract (breaks LSP).
- "Helper modules" with dozens of unrelated methods (violates SRP/ISP).
Tiny checklist (paste in your PR template)
- Does each class have one reason to change?
- If a new variant appears, can I add a class (not edit many)?
- Do polymorphic things share a clear interface and tests?
- Are public APIs small and focused?
- Can I swap dependencies in tests via initializer args?
Installation
Add this line to your application's Gemfile:
gem 'rails-architect'
And then execute:
bundle install
Or install it yourself as:
gem install rails-architect
Usage
Rake Tasks
Run individual principle checks in your Rails application:
bundle exec rake rails_architect:check_solid # Check SOLID principles
bundle exec rake rails_architect:check_kiss # Check KISS principle
bundle exec rake rails_architect:check_dry # Check DRY principle
Or with Docker:
docker exec your_container bundle exec rake rails_architect:check_solid
docker exec your_container bundle exec rake rails_architect:check_kiss
docker exec your_container bundle exec rake rails_architect:check_dry
Example Test Application
The gem includes a test Rails application in test/rails_app/ that demonstrates intentional violations of all principles:
SOLID Violations:
- SRP:
Usermodel has 15+ methods (fat model),UsersControllerhas business logic and 9+ actions - OCP: Deep inheritance chains and case statements that may need modification
- LSP: STI models and polymorphic associations that might violate contracts
- ISP: Heavy concerns and service objects with too many methods
- DIP: Hard-coded external service dependencies and network calls in callbacks
KISS Violations:
- Complexity:
ReportGenerator#generate_complex_reporthas cyclomatic complexity of 14 - Length: Methods over 64 lines with deep nesting (5+ levels)
- Class Size: Large classes with multiple responsibilities
DRY Violations:
- Code Duplication: Identical method implementations across classes
- Repeated Patterns: Same filtering logic in multiple controller actions
- String Literals: Repeated long strings that should be constants
- Method Calls: Excessive calls to the same methods (e.g.,
.countcalled 7+ times)
Configuration:
See test/rails_app/README.md for detailed violation documentation.
Detected Violations
SOLID Principles
Single Responsibility Principle (SRP):
- Models with 15+ public methods (fat models)
- Controllers with 8+ actions (fat controllers)
- Models with 2+ callbacks (callback soup)
- Controllers doing business logic (calling services, mailers)
- Models with network calls in callbacks
Open/Closed Principle (OCP):
- Classes with inheritance depth > 3
- Complex case statements with 3+ conditions
Liskov Substitution Principle (LSP):
- STI models that may violate base contracts
- Polymorphic associations without clear interfaces
Interface Segregation Principle (ISP):
- Controllers including 3+ modules
- ActiveSupport::Concern modules with 5+ methods
- Service objects with 10+ methods
Dependency Inversion Principle (DIP):
- Direct instantiation of classes (
.newcalls > 2) - Hard-coded external service dependencies
- Network calls in model callbacks
KISS Principle
- Methods with cyclomatic complexity > 10
- Classes with > 100 lines
- Methods with > 64 lines
- Nesting depth > 5 levels
DRY Principle
- Identical method implementations across classes
- String literals repeated > 2 times
- Method calls repeated > 5 times (e.g.,
.count,.where) - Duplicate code patterns in controllers
Development
After checking out the repo, run bundle install to install dependencies. Then, run ruby -Ilib:test test/*_test.rb to run the tests.
To install this gem onto your local machine, run bundle exec rake install.
Publishing and Releasing the Gem
Prerequisites
- RubyGems Account: Create an account at rubygems.org if you don't have one
- API Credentials: Ensure you're logged in locally:
bash gem signin
Build the Gem
Build the gem package from the gemspec:
gem build rails-architect.gemspec
This creates a .gem file (e.g., rails-architect-0.1.0.gem) in your project root.
Publishing to RubyGems
Push the built gem to RubyGems:
gem push rails-architect-0.1.0.gem
Or use the rake task:
bundle exec rake release
This rake task will:
- Build the gem
- Create a git tag for the version
- Push the tag to GitHub
- Push the gem to RubyGems
Version Management
Update the version in
lib/rails_architect/version.rb:module RailsArchitect VERSION = "0.2.0" endCommit the version change:
git add lib/rails_architect/version.rb git commit -m "Bump version to 0.2.0"Create and push a git tag:
git tag v0.2.0 git push origin main --tagsBuild and push the gem:
gem build rails-architect.gemspec gem push rails-architect-0.2.0.gem
Yanking a Release
If you need to remove a version from RubyGems:
gem yank rails-architect -v 0.1.0
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/samydghim/rails-architect.
Testing
The gem includes a comprehensive test suite using Minitest. Run tests with:
ruby -Ilib:test test/*_test.rb
Test coverage includes:
- SOLID principles validation (SRP, OCP, LSP, ISP, DIP)
- KISS principle enforcement (complexity, size, nesting)
- DRY principle detection (duplication, patterns, repetition)
- Integration tests with sample Rails app demonstrating violations
- Rake task execution and output formatting