Ruby on Rails πŸš‡

Hero image for Ruby on Rails πŸš‡

All Ruby conventions apply to Ruby on Rails but working with a framework adds another superset of conventions to follow.

PORO stands for Plain Old Ruby Objects and is used to differentiate between objects inheriting functionality from Ruby-on-Rails and those who do not (the latter are POROs).

Naming

  • Suffix each project name with -web or -api depending on the project type.

    # Bad
    rails new project-name
    rails new project-name --api
    
    # Good
    rails new project-name-web
    rails new project-name-api --api
    

    Use the team template for all Ruby on Rails projects.

  • Use kebab-case for naming assets files.

    # Bad
    app/assets/images/user_avatar.svg
    app/assets/stylesheets/components/_card_pricing.scss
    public/assets/images/error_404.svg
    
    # Good
    app/assets/images/user-avatar.svg
    app/assets/stylesheets/components/_card-pricing.scss
    public/assets/images/error-404.svg
    
  • When adding β€œnon-conventional” directories in /app, use the parent directory singular name as a suffix for filenames inside that directory.

    β”œβ”€β”€ forms
    β”‚Β Β  └── create_otp_form.rb
    β”œβ”€β”€ ...
    β”œβ”€β”€ queries
    β”‚Β Β  └── package_query.rb
    β”œβ”€β”€ ...
    └── services
    β”‚Β Β  └── create_user_service.rb
    β”œβ”€β”€ ...
    
  • Use the kebab-case project name as a namespace and prefix to name modules and engines.

    # Bad
    engines/campaign/
    lib/errors/
    
    # Good
    engines/project-name_campaign/
    lib/project-name/errors/
    

Syntax

  • Use Time.current instead of Time.now to use the timezone configured in the Rails environment.

Configuration

  • Use fetch to access environment variables.

    # Bad
    config.action_mailer.default_url_options = {
      host: ENV['DEFAULT_HOST'],
      port: ENV['DEFAULT_PORT']
    }
    
    # Good
    config.action_mailer.default_url_options = {
      host: ENV.fetch('DEFAULT_HOST'),
      port: ENV.fetch('DEFAULT_PORT')
    }
    
  • Provide a default value for non-sensitive variable when possible (i.e. not secret credentials).

    ENV.fetch('NON_EXISTING_NON-SENSITIVE_VARIABLE', 'default value')
    
  • Prefer using an initializer to store environment variables as constants. Name this file as project-name.rb:

    # in config/initializers/project-name.rb
    BASIC_AUTHENTICATION_USERNAME = ENV.fetch('BASIC_AUTHENTICATION_USERNAME')
    BASIC_AUTHENTICATION_PASSWORD = ENV.fetch('BASIC_AUTHENTICATION_PASSWORD')
    
    # in app/controllers/application_controller.rb
    class ApplicationController < ActionController::API
      http_basic_authenticate_with name: BASIC_AUTHENTICATION_USERNAME,
                                  password: BASIC_AUTHENTICATION_PASSWORD
    end
    

    Since this file is loaded on the application boot, it will fail if an environment variable is missing i.e. fast-fail strategy.

Models

  • Code in models must follow the same structure as Ruby classes with additional blocks.

    class SomeModel < ApplicationRecord
      # extend and include custom modules
      extend SomeModule
      include SomeModule
    
      # refinements
      using ArrayExtensions
    
      # third party macros
      acts_as_paranoid
    
      # constants
      SOME_CONSTANT = '20'.freeze
    
      # attributes, enums and accessors
      attr_accessor :name, :last_name
    
      enum status: { inactive: 0, active: 1, blacklisted: 2 }, _suffix: true
    
      store_accessor :social_accounts, %i[facebook linkedin twitter]
    
      # relationships
      has_many :posts, inverse_of: :some_model
      belongs_to :organization, inverse_of: :some_model
    
      # delegations
      delegate :full_name, to: :model_decorator
    
      # validations
      validates :name, presence: true
      validates_with CustomValidatorClass
      validate :custom_validator_method
    
      # scopes
      default_scope { order(first_name: :asc) }
      scope :with_relationship, -> { include(:relationship) }
    
      # callbacks
      after_create_commit :notify_reviewer
    
      # public class methods
      class << self
        def some_method
        end
      end
    
      # initialization
      def initialize
      end
    
      # public instance methods
      def some_public_method
      end
    
      def model_decorator
        @model_decorator ||= ModelDecorator.new(self)
      end
    
      # protected methods
      protected
    
      def some_protected_method
      end
    
      # private methods
      private
    
      def notify_reviewer
      end
    end
    

    When dealing with PORO models, then the default structure for Ruby classes applies.

  • Define explicitly enum keys to avoid both data and implementation inconsistencies. The latter is often caused when non-positive values are needed.

    # Bad
    enum status: [:inactive, :active, :blacklisted]
    
    # Good
    enum status: { inactive: 0, active: 1, blacklisted: 2 }
    enum status: { inactive: -1, active: 0, blacklisted: 1 }
    
  • Use inverse_of option when defining associations to prevent avoidable SQL queries and keep models in sync.

    # Bad
    class Author < ApplicationRecord
      has_many :books
    end
    
    class Book < ApplicationRecord
      belongs_to :author
    end
    
    # Good
    class Author < ApplicationRecord
      has_many :books, inverse_of: :author
    end
    
    class Book < ApplicationRecord
      belongs_to :author, inverse_of: :books
    end
    
  • Use validates with options instead of convenience methods such as validates_option_name_of.

    # Bad
    validates_presence_of :name
    
    # Good
    validates :name, presence: true
    
  • Do not add presentation logic in models, use Decorators instead.

Non-ActiveRecord Models

Not every model is backed by a database. When to use Concerns for appearance, when to encapsulate logic in plain-old Ruby objects. – @dhh

  • Use Single Table Inheritance (STI) to decouple models from database tables and to map better domain concerns to separate objects.

    In its simplest implementation, STI requires a type column and each sub-model to inherit from the same parent class:

    # Bad
    class Transaction < ApplicationRecord
      TRANSACTION_TYPES = %w[Deposit Withdrawal].freeze
    
      validates :type, inclusion: { in: TRANSACTION_TYPES }
    
      def self.create_deposit(attributes = {})
        transaction_type = 'Deposit'
        #...
      end
    
      def self.create_withdrawal(attributes = {})
        transaction_type = 'Withdrawal'
        #...
      end
    end
    
    # Good
    class Transaction < ApplicationRecord
    end
    
    class Deposit < Transaction
    end
    
    class Withdrawal < Transaction
    end
    

    In more complex implementations – where a type column is not possible – STI can be implemented manually using default_scope:

      class Transaction < ApplicationRecord
        enum transaction_type: { deposit: 0, withdrawal: 1 }
      end
    
      class Deposit < Transaction
    
        default_scope { where(transaction_type: :deposit) }
    
        def initialize(attributes = {})
          super(attributes) do
            transaction_type = :deposit
          end
        end
      end
    
  • Use value objects to extract domain concerns.

    class DateRange
      DATE_FORMAT = '%d-%B-%Y'.freeze
    
      attr_reader :start_date, :end_date
    
      def initialize(start_date, end_date)
        @start_date = start_date
        @end_date = end_date
      end
    
      def include_date?(date)
        date >= start_date && date <= end_date
      end
    
      def to_s
        "from #{start_date.strftime(DATE_FORMAT)} to #{end_date.strftime(DATE_FORMAT)}"
      end
    end
    

    Which can be used in an ActiveRecord model:

    class Event < ApplicationRecord
      def date_range
        DateRange.new(start_date, end_date)
      end
    
      def date_range=(date_range)
        self.start_date = date_range.start_date
        self.end_date = date_range.end_date
      end
    end
    
  • Place non-ActiveRecord models – such as value objects but not only – in the directory /models along with ActiveRecord-backed models.

Optimizations

Optimize relationships to avoid n+1 queries:

  • Eager load relationships if needed:

    class Booking < ApplicationRecord
      belongs_to :photographer_package
    
      scope :with_photographer_package, -> { includes(:photographer_package) }
    end
    

    In most cases, it’s better to let Rails optimize the pre-fetching logic using includes but in some cases preload or eager_load must be used directly.

  • Use the gem Bullet to detect n+1 queries.
  • Prefer using find_each with batch_size to process large amount of records.

    # Bad
    User.all.each do |user|
      NewsLetter.deliver(user)
    end
    
    # Good
    User.find_each(batch_size: 500).each do |user|
      NewsLetter.deliver(user)
    end      
    
  • Prefer using exists? over present? to check if a record exists.

    # Bad
    User.where(email: '[email protected]').present?
    
    # Good
    User.exists?(email: '[email protected]')
    
  • Prefer using pluck to get some model attributes as an array.

    # Bad
    active_user_emails = User.where(status: 'active').map(&:email)
    
    # Good
    active_user_emails = User.where(status: 'active').pluck(:email)
    
  • Prefer using select to get the records as an Active Record relation and where the records come with only some specific attributes.

    # Bad
    active_user_emails = User.where(status: 'active')
      
    # Good
    active_user_emails = User.where(status: 'active').select(:id, :name, :email)
    
  • Prefer using insert_all to create many records at once.

    # Bad
    users = [
      { name: 'john', email: '[email protected]' },
      { name: 'ricky', email: '[email protected]' },
      { name: 'maria', email: '[email protected]' }
    ]
    
    users.each do |user|
      User.create(user)
    end
    
    # Good
    users = [
      { name: 'john', email: '[email protected]' },
      { name: 'ricky', email: '[email protected]' },
      { name: 'maria', email: '[email protected]' }
    ]
    
    User.insert_all(users)
    

Routes

  • Prefer using only over except to be more explicit about what routes are available.

    # Bad
    resources :configurations, except: [:new, :create, :destroy]
    
    # Good
    resources :configurations, only: [:index, :show, :edit, :update]
    
  • Prefer using concern to have reusable routes.

    # Bad
    resources :articles do
      resource :comments
    end
      
    resources :photos do
      resources :comments
    end
    
    # Good
    concern :commentable do
      resources :comments
    end
    
    resources :articles, concerns: :commentable
    resources :photos, concerns: :commentable
    
  • Prefer using shallow nesting to avoid deep nested routes.

    # Bad
    resources :articles do
      resources :comments, only: [:index, :new, :create]
    end
    
    resources :comments, only: [:show, :edit, :update, :destroy]
    
    # Good
    resources :articles do
      resources :comments, shallow: true
    end
    
  • Prefer using constraints to validate request at the route level.

    # In the lib/constraint/ip_validation.rb
    module Constraint
      class IPValidation
        def initialize
          @black_listed_ips = Blacklist.retrieve_ips
        end
      
        def matches?(request)
          @black_listed_ips.exclude?(request.remote_ip)
        end
      end
    end
    
    # In the routes.rb
    constraints Constraint::IPValidation.new do
      get 'some_path', to: 'some_controller#action'
    end 
    

Controllers

  • Code in controllers must follow the same structure as Ruby classes with specific blocks.

    class SomeController < ApplicationController
      # extend and include
      extend SomeConcern
      include AnotherConcern
    
      # constants
      SOME_CONSTANT = '20'.freeze
    
      # callbacks
      before_action :authenticate_user!
      before_action :set_resource, only: :index
      before_action :authorize_resource!, only: :index
      before_action :set_requested_resource, only: :show
      before_action :authorize_requested_resource!, only: :show
    
      # public instance methods
      def index
      end
    
      def show
      end
    
      def new
      end
    
      def edit
      end
    
      def create
      end
    
      def update
      end
    
      def destroy
      end
    
      # protected methods
      protected
    
      def current_user
        @current_user = super || NullUser.new
      end
    
      # private methods
      private
    
      def set_resource
      end
    
      def authorize_resource!
      end
    
      def set_requested_resource
      end
    
      def authorize_requested_resource!
      end
    end
    
  • Use strong params to whitelist the list of parameters sent to the controller.

    # Bad
    request.parameters[:user][:name]
    
    # Good
    params.require(:user).permit(:name)
    
  • Do not add business logic in controllers, use Forms, Queries or Services instead.

Views

  • Use local variables instead of instance variables to pass data to views.

    # Bad
    class UsersController
      def show
        @user = User.find(params[:id])
        @user_presenter = UserPresenter.new(@user)
    
        render new
      end
    end
    
    # Good
    class UsersController
      def show
        user = User.find(params[:id])
        user_presenter = UserPresenter.new(@user)
    
        render new, locals: {
          user: user,
          user_presenter: user_presenter
        }
      end
    end
    
  • Do not add presentation logic in views, use Presenters instead.

Project Structure

The structure is consistent with the conventional and built-in structure of Ruby on Rails – which is documented in the official Rails Guide – with the addition of unconventional directories (highlighted with *):

app/
β”œβ”€β”€ assets/
β”œβ”€β”€ channels/
β”œβ”€β”€ controllers/
β”œβ”€β”€ decorators/ *
β”œβ”€β”€ forms/ *
β”œβ”€β”€ helpers/
β”œβ”€β”€ javascript/
β”œβ”€β”€ jobs/
β”œβ”€β”€ mailers/
β”œβ”€β”€ models/
β”œβ”€β”€ policies/ *
β”œβ”€β”€ presenters/ *
β”œβ”€β”€ queries/ *
β”œβ”€β”€ serializers/ *
β”œβ”€β”€ services/ *
β”œβ”€β”€ validators/ *
β”œβ”€β”€ views/
bin/
config/
engines/ *
public/
storage/
vendor/

Decorators

  • Decorators are POROs abstracting view methods from models.

    # Bad
    class User < ApplicationRecord
      def full_name
        "#{first_name} #{last_name}"
      end
    end
    
    # Good
    class User < ApplicationRecord
      delegate :full_name, to: :user_decorator
    
      def user_decorator
        @user_decorator ||= UserDecorator.new(self)
      end
    end
    
    class UserDecorator
      attr_reader :user
    
      def initialize(user)
        @user = user
      end
    
      def full_name
        "#{user.first_name} #{user.last_name}"
      end
    end
    
    # Better
    class User < ApplicationRecord
      delegate :full_name, to: :user_decorator
    
      def user_decorator
        @user_decorator ||= UserDecorator.new(self)
      end
    end
    
    class UserDecorator < SimpleDelegator
      def full_name
        "#{first_name} #{last_name}"
      end
    end
    
  • SimpleDelegator provides the means to delegate all supported method calls to the object passed into the constructor.

Decorators logic is meant to be used solely in backend-related areas e.g. other models, asynchronous jobs, form objects, services… When in need to format data for a view, use a presenter instead.

Forms

  • Forms are POROs abstracting complex logic from controllers.

    class CampaignEnrollingForm
      include ActiveModel::Model
    
      attr_accessor :user
    
      def initialize(user)
        @user = user
      end
    
      def save(params)
        ActiveRecord::Base.transaction do
          user.assign_attributes(params)
          user.charge!
          user.enroll!
    
          raise ActiveRecord::Rollback unless user.save
        end
    
        promote_errors(user.errors)
        errors.empty?
      end
    
      private
    
      def promote_errors(child_errors)
        child_errors.each do |attribute, message|
          errors.add(attribute, message)
        end
      end
    end
    
  • Include ActiveModel::Model to inherit attributes assignments and validations.

  • Use CRUD-like operation to name public methods providing an ActiveRecord-like interface e.g. save, update or destroy.

Read a detailed overview of Form Objects on our blog πŸš€

Policies

  • Policies are POROs handling authorization logic.

    class CampaignPolicy < ApplicationPolicy
      def edit?
        user.completed_onboarding?
      end
    
      def update?
        user.completed_onboarding?
      end
    
      def manage?
        false
      end
    
      alias index? manage?
      alias show? manage?
      alias new? manage?
      alias create? manage?
      alias destroy? manage?
    end
    
  • Prefer using the gem pundit to implement authorization logic.

Presenters

Presenters are POROs abstracting view methods from models, controllers and views.

# Bad
# app/views/users/show.html.slim
span.onboarding__message
  - if user.onboarded?
    = t('users.onboarding.welcome')
  - else
    = t('users.onboarding.welcome_back')

# Good
# app/views/users/show.html.slim
span.onboarding__message
  = user_presenter.onboarding_message

# app/presenters/user_presenter.rb
class UserPresenter
  attr_reader :user

  def initialize(user)
    @user = user
  end

  def onboarding_message
    return I18n.t('users.onboarding.welcome') if user.onboarded?

    I18n.t('users.onboarding.welcome_back')
  end
end

Presenters logic is meant to be used solely in views-related areas e.g. web views, mailers, printed views. When in need to format data for backend purposes, use a decorator instead.

Queries

  • Queries are POROs abstracting complex database fetching, sorting and ordering logic.

    class UsersQuery
      attr_reader :users, :filters
    
      def initialize(users = User.all, filters = {})
        @users = users
        @filters = filters
      end
    
      def call
        @users = date_filtered_users if filter_by_date.present?
        @users = users.includes(:location)
      end
    
      private
    
      def date_filtered_users
        users.where(created_at: filter_start_date..filter_end_date)
      end
    
      def filter_by_date
        filters[:start_date] || filters[:end_date]
      end
    
      def filter_start_date
        filters[:start_date].to_date.beginning_of_day
      rescue ArgumentError, NoMethodError
        start_of_time.beginning_of_day
      end
    
      def filter_end_date
        filters[:end_date].to_date.end_of_day
      rescue ArgumentError, NoMethodError
        Time.current.end_of_day
      end
    
      def start_of_time
        User.order(:created_at).limit(1).first.created_at
      end
    end
    
  • For complex data computation, use database views and materialized views. Prefer using the gem scenic for an efficient integration with ActiveRecord::Migration.

Serializers

  • Serializers are POROs handling API responses rendering.

    class CampaignSerializer
      include JSONAPI::Serializer
    
      attributes :name, :year
    
      has_many :users
      belongs_to :account
    end
    
  • Prefer using the gem jsonapi-serializer to implement Ruby Objects serialization.

Services

Services are POROs handling business logic and the connections between the domain objects.

class CreateChargeService
  def initialize(amount:, token:, **options)
    @amount = amount
    @token = token
    @customer_id = options[:customer_id]
  end

  def call
    raise Nimble::Errors::PaymentGatewayError, charge.failure_message.capitalize unless charge.paid

    charge
  rescue PaymentGateway::Error => e
    raise Nimble::Errors::PaymentGatewayError, e
  end

  private

  attr_reader :amount, :token, :customer_id

  def charge
    @charge ||= PaymentGateway::Charge.create(amount: charge_amount,
                                              currency: Currency.default.iso_code.downcase,
                                              card: token,
                                              customer: customer_id)
  end
end

Validators

Validators are ActiveModel-based validator classes extending ActiveModel validations.

class FutureDateValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors.add(attribute, (options[:message] || :future_date)) unless in_future?(value)
  end

  private

  def in_future?(date)
    date.present? && Time.zone.parse(date.to_s).to_datetime > Time.zone.today
  end
end

Engines

Engines are built-in into Ruby on Rails; even the Rails::Application inherit from Rails::Engine. We use engines to split large monolithic applications into sub-sets of mini-applications. The main benefits:

  • Separation of concerns with clear boundary definitions between domain objects. Even asset manifest files (CSS and JS) are split resulting into better performance and easier maintenance with direct dependencies.
  • Force developers to architect/design the domain objects instead of grouping everything into one main app folder.
  • Easier management of functionality β€œgrowth” over time. The addition of a new feature becomes a matter of deciding in which engine it fits or in rare cases if it requires a new engine.
  • Initial built-in horizontal scalability as each engine can be mounted independently. For instance, it’s possible to run 2 instances for a specific engine (e.g. checkout) but only 1 instance for another engine (e.g. admin) by mounting selectively engines on each instance.
Rails.application.routes.draw do
  mount ProjectNameAdmin::Engine, at: '/', if: ENGINE_ADMIN_ENABLED
  mount ProjectNameCheckout::Engine, at: '/', if: ENGINE_CHECKOUT_ENABLED
end

Engines are not a silver bullet and comes with additional initial architecture and setup costs. So engine-based architecture should be preferred for medium to large applications. For small applications (< 3-5 models and controllers), the conventional architecture – placing all code in app/ – can be sufficient.

  • Use the kebab-case project name as a namespace for all engines for modularization purposes.

    // Bad
    engines/
    β”œβ”€β”€ admin/
    β”œβ”€β”€ checkout/
    β”œβ”€β”€ settings/
    β”œβ”€β”€ website/
    
    // Good
    engines/
    β”œβ”€β”€ project-name_admin/
    β”œβ”€β”€ project-name_checkout/
    β”œβ”€β”€ project-name_setting/
    β”œβ”€β”€ project-name_website/
    

    Make sure to configure the namespace in the engine:

    module ProjectNameCheckout
      class Engine < ::Rails::Engine
        isolate_namespace ProjetNameCheckout
      end
    end
    
  • Use singular to name engines.

    // Bad
    engines/
    β”œβ”€β”€ project-name_campaigns/
    β”œβ”€β”€ project-name_profiles/
    β”œβ”€β”€ project-name_settings/
    
    // Good
    engines/
    β”œβ”€β”€ project-name_campaign/
    β”œβ”€β”€ project-name_profile/
    β”œβ”€β”€ project-name_setting/
    
  • Store shared domain objects – typically almost all models and policies – and functionality – typically stylesheets and JavaScript – into the main app directory. /app becomes the core application. Each engine has its own specific models, policies and assets.

    app/
    β”œβ”€β”€ assets/
    β”‚Β Β  └── stylesheets/
    β”‚Β Β   Β Β  └── core.scss
    β”œβ”€β”€ controllers/
    β”œβ”€β”€ helpers/
    β”‚Β Β  └── svg_helper.rb
    β”œβ”€β”€ javascript/
    β”‚Β Β  └── core.js
    β”œβ”€β”€ jobs/
    β”œβ”€β”€ mailers/
    β”œβ”€β”€ models/
    β”‚Β Β  β”œβ”€β”€ campaign.rb
    β”‚Β Β  β”œβ”€β”€ ...
    β”‚Β Β  └── user.rb
    β”œβ”€β”€ policies/ *
    β”‚Β Β  β”œβ”€β”€ campaign_policy.rb
    β”‚Β Β  β”œβ”€β”€ ...
    β”‚Β Β  └── user_policy.rb
    bin/
    config/
    engines/
    β”œβ”€β”€ project-name_admin/
    β”œβ”€β”€ project-name_checkout/
    β”œβ”€β”€ project-name_settings/
    β”œβ”€β”€ project-name_website/
    public/
    storage/
    vendor/
    
  • Define only shared gems in the root Gemfile. If a gem is used in only one engine, it must be defined in the engine’s gemspec file.

    # In Gemfile
    source 'https://rubygems.org'
    
    ruby '2.5.3'
    
    # Backend
    gem 'rails', '6.0.0' # Latest stable.
    gem 'puma' # Use Puma as the app server.
    
    # Authorizations
    gem 'pundit' # Minimal authorization through OO design and pure Ruby classes.
    
    
    # In engines/project-name_auth/project-name_auth.gemspec
    Gem::Specification.new do |s|
      # ...
    
      s.add_dependency 'devise'
    
      s.add_dependency 'omniauth'
      s.add_dependency 'omniauth-facebook'
      s.add_dependency 'omniauth-twitter'
    end
    
  • Define all engines in the root Gemfile and into a group named engines.

    # Bad
    gem 'project-name_admin', path: 'engines/project-name_admin'
    gem 'project-name_checkout', path: 'engines/project-name_checkout'
    
    # Good
    group :engines do
      gem 'project-name_admin', path: 'engines/project-name_admin'
      gem 'project-name_checkout', path: 'engines/project-name_checkout'
    end
    

    This group can easily be required in application.rb:

    require_relative 'boot'
    
    require 'rails/all'
    
    Bundler.require(:engines, *Rails.groups)
    
  • If a middleware is used in only one engine, it must be defined in the engine’s engine file with middleware.use instead of app.middleware.use

    # Bad
    module ProjectNameAdmin
      class Engine < ::Rails::Engine
        isolate_namespace ProjetNameAdmin
    
        initializer 'project-name_admin.add_middleware' do |app|
          app.middleware.use CustomMiddleware
        end
      end
    end
    
    # Good
    module ProjectNameAdmin
      class Engine < ::Rails::Engine
        isolate_namespace ProjetNameAdmin
    
        initializer 'project-name_admin.add_middleware' do
          middleware.use CustomMiddleware
        end
      end
    end
    
  • Prefer placing all specs in the root /spec directory instead inside of the engine. The main rationale is that all specs are run together and share the same setup and configuration (/spec/support).

    // Bad
    engines/
    β”œβ”€β”€ project-name_admin/
    β”‚Β Β  └── spec/ *
    β”œβ”€β”€ project-name_checkout/
    β”‚Β Β  └── spec/ *
    
    // Good
    engines/
    β”œβ”€β”€ project-name_admin/
    β”œβ”€β”€ project-name_checkout/
    spec/
    β”œβ”€β”€ controllers/
    β”‚Β Β  └── project-name_admin/ *
    β”‚Β Β   Β Β  └── *_spec.rb
    β”‚Β Β  └── project-name_checkout/ *
    β”‚Β Β   Β Β  └── *_spec.rb
    

Database

Prefer to use all practices mentioned in Web - Database.

Documentation

Use YARD docblock to document public methods.

module PaymentGateway
  module V1
    class Account < Resources::BaseResource
      # @param [String] access_token
      # @param [Hash] args
      # @option args [Integer] :version
      # @return [Object]
      def retrieve_identification(access_token, args)
        #...
      end
    end
  end
end