BLUF (Bottom Line Up Front): As a legacy Rails monolith grows, namespaces collapse and domain boundaries blur. The architectural fix is extracting distinct bounded contexts (e.g., billing, reporting) into mountable Rails Engines. This enforces strict isolation, separate routing, and independent test suites without moving to a full microservices architecture.
Phase 1: The Tangled Monolith
In a mature application, models from completely different business domains often call each other directly, creating a web of dependencies that makes safe refactoring impossible.
Synthetic Engineering Context: Boundary Violations
Consider a monolith where the Invoice model directly accesses the User model’s internal authentication logic to determine discount tiers.
# The Bad Code: Boundary Violation
class Invoice < ApplicationRecord
def calculate_discount
# The billing domain should not know about authentication mechanisms
user.last_login_at < 30.days.ago ? 0.10 : 0.0
end
end
Extracting the billing logic into an engine forces you to define a clear API between the core application and the billing module.
Phase 2: Engine Extraction
Generating a mountable engine creates an isolated directory structure with its own app, config, and lib folders.
Execution: Generating the Engine
Use the Rails generator to create the isolated engine structure.
$ bin/rails plugin new components/billing --mountable
This creates a components/billing directory. You must then mount this engine in your main application’s router.
# config/routes.rb (Main App)
Rails.application.routes.draw do
mount Billing::Engine => "/billing"
end
Execution: Isolating Dependencies
Move the relevant models, controllers, and views into the new engine. The engine must declare its own dependencies in its .gemspec file, not the main Gemfile.
# components/billing/billing.gemspec
$:.push File.expand_path("lib", __dir__)
require "billing/version"
Gem::Specification.new do |s|
s.name = "billing"
s.version = Billing::VERSION
# Declare engine-specific dependencies here
s.add_dependency "rails", ">= 6.1.0"
s.add_dependency "stripe", "~> 7.0"
end
Phase 3: Next Steps & Risk Mitigation
Extracting an engine does not automatically fix bad code. If your core application and the new engine share the exact same database tables without clear ownership, you have simply created a distributed monolith. You must establish strict database boundaries.
Need Help Stabilizing Your Legacy App? We architect modular monoliths for scaling enterprises. Our team at USEO untangles complex domain logic and extracts robust Rails Engines.