Setting Up Your Ruby on Rails Monolith for AI Development
Introduction
The AI revolution has arrived, and it's transforming how developers work with codebases. For teams maintaining large Rails monoliths with millions of lines of code, the promise of AI-assisted development is especially appealing. In our experience, these massive codebases can be nearly impossible for AI tools to understand and work with effectively without spending millions of tokens per session.
As software consultants who regularly work with enterprise Rails applications, we've spent the last year experimenting with ways to make large Rails monoliths more accessible to AI development tools like Claude Code, Cursor, and Windsurf. Through trial and error, we've discovered that preparing your codebase for AI requires deliberate architectural choices and workflow adjustments.
Here's our roadmap for successfully setting up your Rails monolith to take full advantage of AI-assisted development.
1. Create Contextual README Files for Major Namespaces
The Problem with Monolithic Documentation
In a massive Rails monolith with millions of lines of code, a single top-level README is essentially useless for AI tools. When an AI assistant tries to understand your billing system, it doesn't need to know about your authentication logic, email processing, or any of the other dozen subsystems in your application. Context windows are limited, and every irrelevant piece of information reduces the AI's ability to provide useful suggestions.
Namespace-Level Context Files
The solution is to create focused documentation files in each major namespace of your application. We recommend creating .context.md or README.md files in key directories throughout your codebase:
```
app/
services/
billing/
.context.md # Explains billing service architecture
notifications/
.context.md # Explains notification system
analytics/
.context.md # Explains analytics pipeline
models/
concerns/
auditable/
.context.md # Explains auditing concern usage
```
These context files should be concise (200-500 lines maximum) and include:
- Purpose: What this namespace is responsible for
- Key Classes: The 5-10 most important classes and what they do
- Data Flow: How data moves through this subsystem
- Dependencies: What external services or other namespaces this code depends on
- Common Patterns: Any namespace-specific patterns or conventions
- Gotchas: Known issues or surprising behavior
By keeping these files focused and co-located with the code they describe, AI tools can quickly load the relevant context when working in a specific area of your codebase. We've found this dramatically reduces token usage and cost while also improving AI suggestion quality by 3-4x compared to relying on a single README.
2. Implement Guard with Fast, Focused Test Suites
The Iteration Speed Problem
AI tools are most effective when developers can rapidly test their suggestions. If your test suite takes 15 minutes to run, you'll find yourself accepting AI-generated code without properly verifying it works or without taking full advantage of how fast code generation can be.
The standard advice is to write faster tests, but with a monolith containing millions of lines, even a well-optimized test suite might take 30+ minutes for a full run. Instead, we need a way to run only relevant tests instantly.
Using Guard for Instant Feedback
Guard is a gem that watches your file system and automatically runs tests when files change. Combined with intelligent test mapping, it creates an instant feedback loop that's perfect for AI-assisted development.
Here's our recommended Guard setup for large Rails monoliths:
# Gemfile
group :development do
gem 'guard'
gem 'guard-rspec'
gem 'spring-commands-rspec'
end
# Guardfile
guard :rspec, cmd: "bundle exec rspec", all_on_start: false do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch(%r{^app/services/(.+)\.rb$}) { |m| "spec/services/#{m[1]}_spec.rb" }
watch(%r{^app/models/(.+)\.rb$}) { |m| "spec/models/#{m[1]}_spec.rb" }
# For controllers, run both controller and request specs
watch(%r{^app/controllers/(.+)\.rb$}) do |m|
[
"spec/controllers/#{m[1]}_spec.rb",
"spec/requests/#{m[1]}_spec.rb"
].compact
end
end
With Guard running in a terminal window, every time you save a file (or accept an AI suggestion), the relevant tests run automatically within 1-3 seconds. This creates a tight feedback loop where the agent can immediately detect if it made a breaking change through a verifiable method that is fast and deterministic.
Optimizing Test Speed
To make Guard truly effective, your individual test files need to run in under 5 seconds. Here are our key strategies:
- Use dependency injection to avoid loading the entire Rails environment for service object tests
- Stub external API calls aggressively using tools like WebMock
- Use build_stubbed instead of create in FactoryBot to avoid database hits when possible
- Break up god objects that require complex test setup
- Keep feature test count low (5-10 for core flows) and rely on faster unit and controller tests
We've found that when individual test files run in under 5 seconds, developers actually trust the AI more because they can verify suggestions instantly.
3. Implement Typed ActiveRecord Models with Sorbet or RBS
Why Types Matter for AI
AI coding assistants are dramatically more effective when working with typed code. In a dynamically-typed Ruby codebase, the AI has to infer types from method names and context, which leads to hallucinations and incorrect suggestions. With explicit types, the AI can provide accurate autocompletions and catch type errors before you even run tests.
Adopting Sorbet Incrementally
Sorbet is a gradual type checker for Ruby, so you don't need to type your entire multi-million line codebase at once. We’ve found a lot of success applying it incrementally, starting with your most critical models and services.
# Before: AI has to guess what fields exist
class Invoice < ApplicationRecord
belongs_to :customer
has_many :line_items
end
# After: AI knows exactly what fields and methods exist
# typed: true
class Invoice < ApplicationRecord
extend T::Sig
sig { returns(Customer) }
attr_accessor :customer
sig { returns(T::Array[LineItem]) }
attr_accessor :line_items
sig { returns(T.nilable(String)) }
attr_accessor :stripe_invoice_id
sig { returns(BigDecimal) }
def total_amount
line_items.sum(&:amount)
end
end
With Sorbet types in place, AI tools can:
- Autocomplete method calls with correct parameters
- Catch type mismatches before you run code
- Understand the data flow through your application
- Generate new methods that match your existing type signatures
Starting Small with Typed Models
We recommend starting by adding Sorbet signatures to:
- Your 10-20 most frequently modified models
- All service objects (since these change often and benefit most from type safety)
- Any complex business logic that AI tools frequently work with
You can use the typed: false comment to opt files out of type checking, allowing you to adopt Sorbet gradually without blocking progress.
4. Set Up Replit Agents for Non-Technical Stakeholders
The Design Iteration Bottleneck
One of the most time-consuming aspects of Rails development is the back-and-forth with designers and product managers. They describe a UI change, you implement it, they realize it's not quite right, and the cycle repeats. This process wastes days of developer time on what should be quick iterations.
Replit Agents for Rapid Prototyping
Replit Agent is an AI-powered development environment that can build functional prototypes from natural language descriptions. For Rails teams, this creates an opportunity: let non-technical stakeholders create their own mockups with hardcoded data, and only bring developers in once the design is validated.
Here's our workflow:
Step 1: Create a Sandboxed Rails Template
Set up a minimal Rails app in Replit with:
- Your company's CSS framework (Tailwind, Bootstrap, etc.)
- Sample models with hardcoded seed data
- Your authentication UI (for consistent navigation)
- No access to production or staging databases
# db/seeds.rb - Hardcoded sandbox data
User.create!(email: "demo@example.com", name: "Demo User")
Invoice.create!(
number: "INV-001",
amount: 1000.00,
status: "paid",
customer: Customer.first
)
Step 2: Product Manager Creates Mockup with AI
The product manager describes what they want to Replit Agent:
"Create an invoice list page showing invoice number, customer name,
amount, and status. Add filters for status (paid/unpaid/overdue) and
a date range picker. Use Tailwind CSS to match our design system."
Replit Agent generates a functional page in minutes. The product manager can iterate on the design, try different layouts, and get feedback from stakeholders—all without developer involvement.
Step 3: Developer Implements Production Version
Once the design is validated, a developer extracts the relevant view code and implements it properly with:
- Real database queries instead of hardcoded data
- Proper authorization checks
- Pagination and performance optimizations
- Tests
This approach reduces back-and-forth by 70% because all the design iteration happens before developer time is involved.
Security Considerations
Make sure your Replit sandbox:
- Uses completely separate, fake data (never production database credentials)
- Has no API keys or secrets that could access real systems
- Is clearly labeled as a "Design Sandbox" to avoid confusion
- Gets recreated from the template regularly to prevent drift
We've found this particularly valuable for customer-facing UI work where design aesthetics matter most, and where non-technical stakeholders have strong opinions that are hard to communicate through mockups or tickets.
5. Build an AI-Friendly Service Layer Architecture
Why Controllers Confuse AI Tools
AI assistants struggle with Rails controllers because they mix multiple concerns: params parsing, authorization, business logic, and rendering. When an AI tries to understand or modify a controller, it has to track all these concerns simultaneously, leading to suggestions that miss authorization checks or break rendering logic.
The Service Object Pattern
Moving business logic into service objects makes your code dramatically more AI-friendly. Service objects are plain Ruby classes with a single responsibility and an explicit interface—exactly what AI tools work best with.
Before: Complex Controller Logic
class InvoicesController < ApplicationController
def create
authorize! :create, Invoice
customer = Customer.find(params[:customer_id])
invoice = customer.invoices.build(invoice_params)
if invoice.save
InvoiceMailer.invoice_created(invoice).deliver_later
Analytics.track(current_user, 'invoice_created', invoice.id)
if params[:send_to_stripe]
stripe_invoice = StripeService.create_invoice(invoice)
invoice.update(stripe_invoice_id: stripe_invoice.id)
end
render json: invoice, status: :created
else
render json: invoice.errors, status: :unprocessable_entity
end
end
end
After: Service Object with Clear Interface
# app/services/billing/invoice_creator.rb
module Billing
class InvoiceCreator
attr_reader :customer, :invoice_params, :send_to_stripe
def initialize(customer:, invoice_params:, send_to_stripe: false)
@customer = customer
@invoice_params = invoice_params
@send_to_stripe = send_to_stripe
end
def call
invoice = customer.invoices.create!(invoice_params)
send_notifications(invoice)
sync_to_stripe(invoice) if send_to_stripe
invoice
end
private
def send_notifications(invoice)
InvoiceMailer.invoice_created(invoice).deliver_later
Analytics.track(customer.user, 'invoice_created', invoice.id)
end
def sync_to_stripe(invoice)
stripe_invoice = StripeService.create_invoice(invoice)
invoice.update(stripe_invoice_id: stripe_invoice.id)
end
end
end
# app/controllers/invoices_controller.rb
class InvoicesController < ApplicationController
def create
authorize! :create, Invoice
customer = Customer.find(params[:customer_id])
invoice = Billing::InvoiceCreator.new(
customer: customer,
invoice_params: invoice_params,
send_to_stripe: params[:send_to_stripe]
).call
render json: invoice, status: :created
rescue ActiveRecord::RecordInvalid => e
render json: e.record.errors, status: :unprocessable_entity
end
end
With this structure, AI tools can:
- Understand the InvoiceCreator service in isolation without controller concerns
- Generate accurate tests for the service without mocking Rails dependencies
- Suggest modifications to business logic without breaking authorization or rendering
- Work with the .context.md file in app/services/billing/ to understand the full billing flow
Service Object Conventions for AI
To maximize AI effectiveness with service objects, we follow these conventions:
- Always use keyword arguments so AI tools understand parameter names
- Keep service objects under 100 lines to fit in AI context windows
- Use explicit return values instead of instance variables
- Namespace by domain (Billing, Auth, Notifications) for better context files
- Follow the Command pattern with a #call method as the main entry point
These conventions create predictable patterns that AI tools can learn and replicate accurately.
Conclusion
Preparing a massive Rails monolith for AI-assisted development isn't about adopting the fanciest AI tools from social media, it's about making your codebase more understandable and modular.
We've seen teams reduce time-to-feature by an order of magnitude after implementing these practices. The initial investment in restructuring and documentation pays dividends within weeks as AI tools begin providing genuinely useful suggestions instead of plausible-sounding nonsense without requiring millions of tokens per session.
The future of Rails development is a partnership between human expertise and AI assistance. By following these five tips, you'll ensure your monolith is ready to take full advantage of that partnership.
Have you tried any of these techniques in your Rails monolith? We'd love to hear about your experiences with AI-assisted development. Reach out to share your story or if you need help preparing your codebase for the AI era.
