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 with ActiveModel::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::Concern mixing 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: User model has 15+ methods (fat model), UsersController has 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_report has 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., .count called 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 (.new calls > 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

  1. RubyGems Account: Create an account at rubygems.org if you don't have one
  2. 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

  1. Update the version in lib/rails_architect/version.rb:

    module RailsArchitect
     VERSION = "0.2.0"
    end
    
  2. Commit the version change:

    git add lib/rails_architect/version.rb
    git commit -m "Bump version to 0.2.0"
    
  3. Create and push a git tag:

    git tag v0.2.0
    git push origin main --tags
    
  4. Build 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