This content originally appeared on DEV Community and was authored by Alex Aslam
There comes a moment in the life of every substantial Rails application. You stand before the monolith, a cathedral of code you’ve painstakingly built. You know every nook, every cranny. But now, the echoes of distant teams get lost in its vast nave. A change in the Billing module inexplicably shatters the Content management system. The once-clear structure feels more like a sprawling, interconnected maze.
You long for sanctuaries. Self-contained chapels within your grand cathedral. Places where a dedicated team can worship their domain logic without fear of toppling a gargoyle in another part of the edifice.
This is the journey to Rails Engines. This isn’t about microservices and the chaos of network partitions; this is about bringing order, clarity, and architectural grace to your monolith. It’s the art of building mini-apps within your kingdom.
The Catalyst: The Cracks in the Foundation
You feel the pain long before you name it.
- The “App/Everything” Bloat: Your
app/modelsdirectory is a bottomless pit.Useris a 1200-line god object, responsible for authentication, profile management, and newsletter preferences. - Team Treadmills: The “Billing Team” and the “Content Team” step on each other’s toes daily. A merge conflict in
routes.rbis a weekly ceremony. The cognitive load is immense. - The Test Molasses: To run a simple unit test for a
BlogPost, your test suite has to boot the entire universe—including the payment gateway and the Redis cache for analytics. It’s slow, and it’s wrong. - Feature Entanglement: You can’t imagine shipping the “Pro” version of your product without the “Enterprise” features bleeding in. The conditional logic is a spiderweb.
When you see this, you’re not facing an engineering problem alone. You’re facing an architectural one. You need boundaries.
The Blueprint: What Is a Rails Engine?
An Engine, in its essence, is a miniature Rails application that lives inside, and mounts itself onto, a host Rails application. It has its own app/ directory, its own config/routes.rb, its own models, controllers, and views.
Think of it not as a gem you call, but as a plugin you inhabit.
The host application, the parent kingdom, grants the Engine, a semi-autonomous duchy, a piece of its land (a URL namespace) and says: “Rule this domain. Manage your own affairs. But know you are part of this greater whole.”
The Artisan’s Tools: --mountable and isolate_namespace
The magic is conjured with two simple spells.
When you generate your engine:
rails plugin new Billing --mountable --full
The --mountable flag is the key. It does two critical things:
- It creates an isolated namespace for all the Engine’s components. You won’t have a
Billing::Subscriptionmodel conflicting with a potentialSubscriptionmodel in the host app. Everything is scoped. - It generates a dedicated
Engineclass (inlib/billing/engine.rb) that defines the lifecycle of your mini-app.
The heart of this isolation is in that engine file:
# lib/billing/engine.rb
module Billing
class Engine < ::Rails::Engine
isolate_namespace Billing # This is the line that builds the walls.
end
end
isolate_namespace is the commandment that establishes the sanctum. It tells Rails: “Keep my models, my routes, my tables, separate. Contain my essence.”
The Journey of Creation: A Practical Grimoire
Let’s build the Billing sanctum.
1. The Genesis
rails plugin new billing --mountable --full
cd billing
Your structure now mirrors a full Rails app, but under billing/:
billing/
app/
controllers/
billing/
application_controller.rb
models/
billing/
application_record.rb
views/
layouts/
billing/
application.html.erb
config/
routes.rb
lib/
billing/
engine.rb # The Heart
version.rb
billing.rb # The Soul
2. Crafting the Domain
You build your engine’s world as you would any Rails app.
# billing/app/models/billing/subscription.rb
module Billing
class Subscription < ApplicationRecord
# This is Billing's Subscription. The host app knows nothing of it.
has_many :invoices
# ... pure, focused billing logic
end
end
# billing/app/controllers/billing/subscriptions_controller.rb
module Billing
class SubscriptionsController < ApplicationController
# This controller is scoped to the Billing world.
def index
@subscriptions = Subscription.all
end
end
end
3. Defining the Gateway
Your engine has its own routes, a map of its internal kingdom.
# billing/config/routes.rb
Billing::Engine.routes.draw do
# These routes are contained within the engine
resources :subscriptions
root to: "subscriptions#index"
end
4. The Integration: Mounting the Sanctum
In your host application’s Gemfile:
gem 'billing', path: '../billing' # For local development
And in the host’s config/routes.rb, you grant it land:
# config/routes.rb of the Host App
Rails.application.routes.draw do
mount Billing::Engine, at: '/billing'
# ... your host app's other routes
end
And just like that, your Billing engine is alive at yoursite.com/billing. All its routes are prefixed with this path. The sanctum has its own entrance.
The Master’s Challenges: Shared Resources and Cross-Border Diplomacy
An Engine is not an island. It’s a semi-autonomous region. It needs to interact with the host.
1. The User Problem: Your Billing engine needs to know about the User. But the User model lives in the host. Do you reference it directly? No. That would break the isolation.
The elegant solution is Dependency Injection. The host app provides the user to the engine.
# In the host application's initializer
Billing.configure do |config|
config.user_class = 'User' # A string, not a constant
config.current_user_method = :current_user # The method in the host's ApplicationController
end
# Inside the Billing engine's controller
current_user = send(Billing.config.current_user_method)
user_class = Billing.config.user_class.constantize
2. The Database Problem: The engine needs its own tables. How do we share the database?
Rails Engines have a beautiful convention for this. You place migrations in your billing/db/migrate directory. Then, you instruct the host app to load them.
# In billing/lib/billing/engine.rb
initializer "billing.load_migrations" do |app|
app.config.paths["db/migrate"] += paths["db/migrate"].existent
end
Now, when you run bin/rails db:migrate in the host app, it seamlessly runs your engine’s migrations too. The data is in one database, but the schemas are logically separated by table names (e.g., billing_subscriptions).
The Payoff: The Architecture of Serenity
When you complete this journey, what have you gained?
- Radical Focus: The Billing team works solely within the
billing/directory. Their models, controllers, views, and tests are all together. The cognitive load plummets. - True Encapsulation: The Billing domain is a black box to the host. It exposes a clear, versioned API. Its internal changes are its own.
- Team Autonomy: Teams can develop, test, and even version their engines independently, integrating them into the main app like well-crafted library books.
- Strategic Optionality: That “Enterprise” feature suite? It can be a separate engine, mounted only for certain deployments. Your product becomes a composition of plugins.
The Final Brushstroke
Rails Engines are not a silver bullet. They add complexity. The initial setup requires thought. But for the senior developer staring at a complex, team-laden monolith, they are the ultimate tool for imposing order upon chaos.
They allow you to transition from a single, tangled codebase to a curated collection of focused, collaborative mini-apps. You are no longer just a developer in a maze; you are the architect, designing sanctums within your cathedral.
Go forth. Draw your boundaries. Build your sanctums. Your monolith will thank you for it.
This content originally appeared on DEV Community and was authored by Alex Aslam