# Generate engine
# rails plugin new billing --mountable
# lib/billing/engine.rb
module Billing
class Engine < ::Rails::Engine
isolate_namespace Billing
config.generators do |g|
g.test_framework :rspec
g.fixture_replacement :factory_bot
end
# Load migrations from engine
initializer 'billing.load_migrations' do |app|
unless app.root.to_s.match root.to_s
config.paths['db/migrate'].expanded.each do |expanded_path|
app.config.paths['db/migrate'] << expanded_path
end
end
end
# Load engine routes
initializer 'billing.routes' do |app|
app.routes.prepend do
mount Billing::Engine => '/billing'
end
end
end
end
# Engine routes (billing/config/routes.rb)
Billing::Engine.routes.draw do
resources :subscriptions do
member do
post :cancel
post :reactivate
end
end
resources :invoices, only: [:index, :show]
resources :payment_methods
end
# Engine controller
module Billing
class SubscriptionsController < ApplicationController
before_action :authenticate_user!
def index
@subscriptions = current_user.subscriptions
end
def create
@subscription = current_user.subscriptions.build(subscription_params)
if @subscription.save
redirect_to billing.subscription_path(@subscription)
else
render :new
end
end
def cancel
@subscription = current_user.subscriptions.find(params[:id])
@subscription.cancel!
redirect_to billing.subscriptions_path
end
private
def subscription_params
params.require(:subscription).permit(:plan_id, :payment_method_id)
end
end
end
# Engine model
module Billing
class Subscription < ApplicationRecord
belongs_to :user, class_name: '::User'
belongs_to :plan
enum status: { active: 0, canceled: 1, expired: 2 }
def cancel!
update!(
status: :canceled,
canceled_at: Time.current
)
end
end
end
# Parent app - mount engine in config/routes.rb
Rails.application.routes.draw do
mount Billing::Engine => '/billing', as: 'billing'
mount AdminPanel::Engine => '/admin'
end
# Accessing engine routes from parent app
billing.subscriptions_path
# => /billing/subscriptions
billing.subscription_path(@subscription)
# => /billing/subscriptions/1
# Accessing parent app routes from engine
main_app.root_path
main_app.user_path(@user)
# Sharing models between parent app and engine
# Parent app user model:
class User < ApplicationRecord
has_many :subscriptions, class_name: 'Billing::Subscription'
end
# Engine accessing parent model:
module Billing
class Subscription < ApplicationRecord
belongs_to :user, class_name: '::User'
end
end
# Engine configuration
# Parent app can configure engine in initializer
# config/initializers/billing.rb
Billing.configure do |config|
config.stripe_api_key = ENV['STRIPE_API_KEY']
config.webhook_secret = ENV['STRIPE_WEBHOOK_SECRET']
end
# lib/billing.rb (in engine)
module Billing
class << self
attr_accessor :configuration
end
def self.configure
self.configuration ||= Configuration.new
yield(configuration)
end
class Configuration
attr_accessor :stripe_api_key, :webhook_secret
def initialize
@stripe_api_key = nil
@webhook_secret = nil
end
end
end
# Installing engine migrations
# rake billing:install:migrations
# rake db:migrate
# Engine decorators/overrides
# Parent app can override engine behavior
# app/decorators/billing/subscription_decorator.rb
Billing::Subscription.class_eval do
def custom_method
# Add behavior to engine model
end
end
# Engine dependencies in gemspec
# billing.gemspec
Gem::Specification.new do |spec|
spec.name = "billing"
spec.version = Billing::VERSION
spec.authors = ["Your Name"]
spec.summary = "Billing engine"
spec.files = Dir["{app,config,db,lib}/**/*"]
spec.add_dependency "rails", "~> 7.0"
spec.add_dependency "stripe"
spec.add_development_dependency "rspec-rails"
spec.add_development_dependency "factory_bot_rails"
end
# Testing engine
# spec/dummy app is a minimal Rails app for testing
# billing/spec/rails_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../dummy/config/environment.rb', __FILE__)
RSpec.describe Billing::Subscription do
it 'cancels subscription' do
subscription = create(:billing_subscription, status: :active)
subscription.cancel!
expect(subscription.status).to eq('canceled')
end
end
# Publishing engine as gem
# gem build billing.gemspec
# gem push billing-0.1.0.gem
Rails engines are miniature Rails applications within applications. I use engines for extracting reusable functionality—authentication, billing, admin panels. Engines have their own models, controllers, views, routes, migrations. Mountable engines are fully isolated; regular engines share parent app's namespace. Engines enable modular monoliths—separation without microservices complexity. Testing engines independently ensures they're truly decoupled. Engines can be extracted to gems for sharing across projects. Engine migrations integrate with parent app via rake engine_name:install:migrations. Understanding engine isolation levels—full vs. partial—is key. Engines are powerful for large apps needing clear boundaries and reusability.