The Rails SaaS Architecture I Wish I Had 5 Years Ago
Five years ago, I started building SaaS applications with Ruby on Rails.
Like most Rails apps, everything started beautifully simple.
A few models.
A few controllers.
Some concerns.
Some service objects.
It felt clean.
Six months later?
- Business logic everywhere
- “Temporary” modules that became permanent
- Cross-cutting features tangled across the app
- Shared helpers doing too much
- Growing fear of refactoring core features
The app still worked — but the architecture wasn’t intentional anymore.
And that’s the real problem.
The Pattern I Kept Repeating
Every SaaS I built ended up having the same core capabilities:
- Authentication
- Roles & permissions
- Notifications
- Audit logs
- Dashboards
- Support tickets
- Document management
- Account scoping / multi-tenancy
But I kept rebuilding them inside the main app.
Every. Single. Time.
And every time, they slowly leaked into everything else.
The Architecture Shift
At some point I started asking myself:
What if SaaS capabilities weren’t just folders inside
/app?
What if they were isolated systems?
Instead of structuring only by MVC, I started structuring by capability.
Each major SaaS feature became its own Rails engine:
- Auth engine
- Support engine
- Audit engine
- Dashboard engine
- Admin engine
Each engine:
- Owns its models
- Owns its controllers
- Owns its routes
- Owns its views
- Owns its database tables
- Has explicit integration points
No leaking constants.\
No random cross-feature helpers.\
No accidental coupling.
Just clear boundaries.
Why Engines?
Rails engines are often misunderstood.
Many developers think they’re only for gems or mountable admin panels.
But for SaaS architecture, they provide something incredibly valuable:
Structural isolation.
When features live in engines:
- You can reason about them independently
- You can test them in isolation
- You can disable or replace them
- You reduce accidental coupling
- You avoid the “god app” problem
The main Rails app becomes the orchestrator — not the dumping ground.
What This Changes in Practice
Instead of this:
app/models/user.rb
app/models/role.rb
app/models/ticket.rb
app/models/audit_log.rb
You get something like this:
engines/auth/app/models/auth/user.rb
engines/support/app/models/support/ticket.rb
engines/audit/app/models/audit/log.rb
Clear ownership.
Clear responsibility.
Clear boundaries.
The Real Benefit: Cognitive Load
The biggest improvement wasn’t performance.
It wasn’t scalability.
It was mental clarity.
When a bug appears in support logic, I know exactly where to go.
When I need to extend auditing, I don’t fear breaking unrelated features.
Architecture stops being accidental.
It becomes intentional.
Is This Over-Engineering?
For small apps?
Probably.
For serious SaaS products?
Not really.
Once your application has:
- Multiple accounts
- Role systems
- Permissions
- Notifications
- Background processing
- Operational tooling
You’re no longer building “just a Rails app.”
You’re building infrastructure.
What I’m Doing Now
I’m currently building an open-source Rails framework based on this idea.
Each SaaS capability lives in its own engine.
The goal isn’t to abstract everything.
The goal is to start with intentional structure — instead of cleaning up chaos later.
It’s still evolving.
I’m still learning.
But it already feels more sustainable than anything I built five years ago.

Top comments (2)
Great article!
Mental Clarity is the key to all good architecture .
Domain-level restructuring is exactly what I'm focusing on for my products right now. Engines seem like a fantastic approach.
I am interested to see how things go with your framework.
Appreciate that 🙌
Once features are isolated by capability instead of just folders, the whole system becomes easier to reason about.
I’ll definitely share more as the framework evolves.