我如何构建模块化的 Rails SaaS 应用程序

发布: (2026年3月11日 GMT+8 11:00)
10 分钟阅读
原文: Dev.to

Source: Dev.to

Rails SaaS architecture diagram

在我之前的文章中,我写到了我 5 年前本希望拥有的 Rails SaaS 架构。
核心思想很简单:当一个 Rails 应用成长为真正的 SaaS 产品时,问题不再只是快速编写功能。

真正的挑战变成了 让系统易于理解

这自然会引出下一个问题:

在实际操作中,这种结构到底是什么样子的?

这篇文章是我对这个问题的尝试性回答。它 并不是 组织 Rails 应用的唯一方式;它只是目前对我而言,在具备多业务能力、内部工具以及长期增长需求的产品中最合理的结构。

默认增长路径的问题

大多数 Rails 应用程序最初都有一个非常合理的结构:

app/
config/
db/
lib/

在起步阶段这运作良好。

但随着产品的增长,许多业务功能开始并列存在于同一应用层:

  • 身份验证
  • 角色和权限
  • 通知
  • 仪表盘
  • 审计
  • 支持工单
  • 文件管理
  • 计费逻辑
  • 管理工具

此时,问题不在于 Rails 本身,而是所有东西开始在同一个应用边界内部争夺空间。

  • 模型了解的东西太多。
  • 控制器开始协调不相关的关注点。
  • 辅助方法向奇怪的方向发展。
  • Concern 变得层出不穷。

最终,即使各个功能本身并不复杂,整个应用也会感觉庞大。

转变:按能力结构化

对我而言,更有效的做法是按 业务能力 而不是仅按技术层来结构化系统。

因此,我不再仅仅从以下角度思考:

  • models
  • controllers
  • views

我从以下角度思考:

  • 支持
  • 审计
  • 管理员
  • 账户
  • 用户
  • 仪表盘
  • 计费

每个能力都有自己的边界。在 Rails 中,我发现最简洁的实现方式是使用 engines

简单的高级结构

一个模块化的 Rails SaaS 应用可能如下所示:

my_app/
├── app/
├── config/
├── db/
├── lib/
├── engines/
│   ├── lesli_core/
│   ├── lesli_admin/
│   ├── lesli_audit/
│   ├── lesli_billing/
│   ├── lesli_dashboard/
│   ├── lesli_shield/
│   └── lesli_support/
└── Gemfile

主应用仍然存在,但它不再是把所有功能都塞进去的地方。
相反,主应用更像是 集成层,而各个引擎则包含实际的业务能力。

主应用程序中应包含什么?

这是最关键的部分。模块化结构只有在主应用保持纪律时才有效。就我而言,主 Rails 应用通常负责:

  • environment configuration
  • deployment configuration
  • boot process
  • mounting engines
  • app‑specific branding and overrides
  • product‑specific custom logic
  • final composition of the system

因此,应用仍然重要——只是它不再假装直接拥有每个领域。

什么属于引擎?

每个引擎都有明确的职责。例如,一个 support(支持)引擎可能包含:

engines/lesli_support/
├── app/
│   ├── controllers/
│   ├── models/
│   ├── views/
│   └── components/
├── config/
│   └── routes.rb
├── db/
│   └── migrate/
├── lib/
│   └── lesli_support/
└── lesli_support.gemspec

该引擎可以管理:

  • 工单
  • 评论或讨论
  • 状态
  • 优先级
  • 分配流程
  • 支持专用的仪表盘
  • 与支持相关的通知

这会产生极其有价值的东西:本地推理。当我需要处理支持相关的功能时,我会直接进入 support 引擎,而不是在庞大的应用中四处搜寻,试图记起工单逻辑泄漏到了哪些层。

Source:

核心层的作用

我仍然喜欢拥有一个共享的核心层,但它应该保持 小而有目的。对我来说,核心引擎通常包含以下内容:

  • 真正跨越多个模块的共享关注点
  • 共享的 UI 基元
  • 基类
  • 常用辅助函数
  • 平台配置辅助工具
  • 引擎之间的通用接口

我尽量避免把核心层变成第二个单体。如果 core 成为每个引擎都倾倒共享快捷方式的地方,架构就会慢慢回到同样的问题。因此我不断自问:

这真的跨越多个模块,还是我只是在回避更清晰的边界?

路由与组合

我喜欢这种方法的一点是组合保持显式。主应用决定挂载什么。下面是一个简化的例子:

# config/routes.rb
Rails.application.routes.draw do
  mount LesliAdmin::Engine    , at: "/admin"
  mount LesliSupport::Engine  , at: "/support"
  mount LesliAudit::Engine    , at: "/audit"
end

这使得最终应用的结构易于理解。产品并不是谜团;它是各种功能的组合。

边界比复用更重要

引擎的一个不错的副作用是可以复用,但这 并不是 我喜欢它们的主要原因。更大的好处是 强制性的边界

  • 没有边界时,每个特性最终都会渗透到其他所有地方。
  • 有了边界,集成就必须是有意为之。

这会改变代码库的增长方式。你会开始提出更好的问题:

  • 这段逻辑应该属于已有的引擎,还是应该拥有自己的能力?
  • 引擎之间如何通信而不产生紧耦合?
  • 什么真正属于核心层,什么属于特定的引擎?

把这些问题放在首位,架构就能保持模块化、可维护,并为长期增长做好准备。

这种依赖可能存在吗?

  • 支持真的需要了解计费内部细节吗?
  • 这个关注点是共享的,还是仅仅放错了位置?
  • 这些逻辑应该放在应用层、引擎层,还是核心层?

这些问题比任何命名约定都更能提升架构。

这不是免费

公平地说,这种结构也会带来一些额外开销。你需要更多地考虑以下方面:

  • 引擎命名
  • 领域边界
  • 依赖方向
  • 跨引擎迁移
  • 共享约定
  • 本地开发工作流

因此,我 不会 在每个项目中都使用这种方式。

如果你在构建一个小型内部工具、MVP,或是需要非常快速交付的东西,这种结构可能会显得过早且繁琐。

但当产品开始在多个功能上扩展时,这额外的结构就不再显得沉重,反而变得非常有用。

我最喜欢这种方法的原因

我最喜欢的并不是它看起来更“高级”。
而是它让系统更易于使用。

  • 当我添加新功能时,我能更清楚它应该放在哪里。
  • 当我调试问题时,我不必在应用的无关部分之间跳来跳去。
  • 当我重构时,我会更有信心不会把周围的一切都弄坏。

对我而言,这才是良好架构应有的作用。

  • 不在于赢得抽象分数。
  • 不在于在图表上显得惊艳。
  • 只是在应用不断增长时,让它更易于理解。

How This Connects to Lesli

Lesli architecture diagram

这与我在构建 Lesli 时所探索的总体方向相同。

我并不是想让 Rails 变得更复杂。
我主要是想为更大型的 SaaS‑style 应用提供一个更清晰的成长空间。

Lesli 仍在不断发展,但这种模块化方法是其背后的理念之一。

0 浏览
Back to Blog

相关文章

阅读更多 »

无惧分支

Git Mastery 系列第 3 部 ← 第 2 部:Committing with Intention https://dev.to/itxshakil/committing-with-intention-the-art-of-a-good-commit-p90 | 第 4 部:C…