Over the years of working with large, mature, monolithical Rails applications, I realized a subtle problem. Many, even experienced teams put their constants in suboptimal namespaces. On one hand such placement is logical, but on the other it leads cluttered code that mixes low-level and high-level concepts. It does not help with keeping our monolith modularized either.

Consider a common scenario in a Rails application: the Team class. As your application evolves, you might find yourself adding constants like this:

class Team < ApplicationRecord
  SMB_SALES = :smb
  ENTERPRISE_SALES = :ent
  US_SUPPORT = :us_support
  EU_SUPPORT = :eu_support

  has_many :memberships
  has_many :members, through: :memberships
end

Initially, this approach seems harmless. Other parts of your codebase might use these constants like so:

Team.find_by(identifier: Team::SMB_SALES)

Over time, you might even add convenience methods like Team.smb or User.smb_sales. However, as your company grows and adds more divisions, teams, and units, the Team class and team.rb file keep expanding. The core issue here is that the Team class itself doesn’t typically need to know about these specific team identifiers. It’s a low-level class that should remain generic and unaware of the higher-level domains built on top of it.

Do you know which classes/modules care about those constants? The ones that use them. Sales logic cares about the Sales team. Accounting logic cares about the accountants team. Contracting logic cares about the legal team.

The high-level domains of your app (payments, sales) need to use the low level functionalities (authentication, teams, notifications). The low level code and functionalities should remain unaware of what’s build on top of them. Their code and terminology should be generic. There is no need for the Team class to be aware of sales or marketing. Instead the dependency is in the other direction.

Understanding the Impact

This approach leads to several problems:

  • Cluttered Code: The Team class becomes bloated with constants that it doesn’t directly use.
  • Mixed Concerns: Low-level concepts (business units) get mixed with high-level business logic (sales, support divisions).
  • Reduced Modularity: It becomes harder to separate concerns and modularize your application.

The alternative solution

Instead of adding constants to the Team class, consider moving them to more appropriate namespaces or engines. Here’s an improved approach:

module Sales
  SMB_SALES_TEAM_IDENTIFIER = :smb
  ENTERPRISE_SALES_TEAM_IDENTIFIER = :ent
end

module Support
  US_TEAM_IDENTIFIER = :us_support
  EU_TEAM_IDENTIFIER = :eu_support
end

This approach ensures that constants related to various domains don’t accumulate in the low-level Team class.

For even more flexibility and encapsulation, consider creating domain-specific classes:

module Sales
  class Smb
    TEAM_IDENTIFIER = :smb

    def self.team
      Team.find_by(identifier: TEAM_IDENTIFIER)
    end

    # other common methods
  end
end

What’s the benefit

The benefits are mostly visible in enterprise Rails applications which use certain approach for development scalability such as:

  • code ownership and mandatory code reviews when external teams change your team’s codebase.
  • dependency tracking between domains/engines, i.e. by using packwerk

Under such constraints, we can assume:

  • Reduced Code Review Overhead: engineering teams owning low-level classes like Team aren’t burdened with a stream of reviews for pull requests that keep adding to team.rb file.
  • Improved Code Organization: Constants are placed closer to where they’re actually used.
  • Clearer Dependencies: Tools like packwerk can track dependencies between domains more accurately. Code that uses Sales::SMB_SALES_TEAM_IDENTIFIER will be tracked as having a dependency on the Sales engine. This info can be used to either track valid dependencies or to discover unwanted coupling between certain domains.

By adopting this approach, we can create more maintainable and scalable Rails apps, especially as they grow into enterprise-level system.