My Method for Rails Model Namespacing

Last updated on March 16, 2021.

Namespaces are one of the simplest and most powerful tools developers have to keep a growing domain model organized, but without a specific formula for how to apply this tool, Rails developers will find themselves having to make a number of decisions along the way, and the consequences of those decisions aren't always clear when you're running rails g model.

Running into unforeseen snags and having to undo your work, or uncertainty about the best approach means many developers skip namespacing altogether.

After years of trying to figure out an approach to namespacing that would yield the best results in new and growing Rails apps, I think in the course of the last year, I finally cracked a simple set of rules that reliably yield the results I was always looking for.

I've failed at utilizing namespaces so many times and in so many different ways. In my earliest attempts, when I first started developing with Rails, I'd simply give up on using namespaces at all. In later attempts, I stuck to my guns and used them, but missteps in my approach meant I actually ended up interacting with a domain model that felt like a step back from just having everything in a global namespace.

This article is an attempt to document the simple, formulaic approach to namespacing that has finally yielded the results I had always been looking for. My hope in documenting it is two-fold: To invite discussion on the topic so I can consider what other approaches might yield similarly good results, and also to aid other teams of developers who might just want an approach to subscribe to so they don't have to hash out every detail and engage in the same trial and error that led to these results.

So let's get started!

Don't put models that represent the subject of a namespace in their own namespace.

Say we know we're going to have a subscriptions subsystem in our application. We all know that subsystem is going to have a bunch of related models, (e.g. Plan,) and it would make sense to put those models in a Subscriptions:: namespace.

But what should we do with the core concept of a subscription? Should we just call it Subscription or should we namespace it as well? Should it be Subscriptions::Subscription?

I think the default answer to this question should be:

No, don't put the primary subject of a namespace in it's own namespace.

Why? Isn't the whole point of namespacing to keep everything together?

Yes, absolutely, but I believe leaving the primary subject of a namespace one level higher than the namespace dedicated to it allows namespaces to be employed more easily in the future and with less foresight required when initially modeling our domain.

Forgive me, but I'm going to start with a more complicated example, and then follow-up with a really straight forward one.

The Complicated Example

Say we know there are a bunch of subscription-related models, and you know that you want to put them in a namespace, and you end up modeling the concept of a subscription itself as Subscriptions::Subscription (which I'm saying we shouldn't) and maybe it has a relationship to the chosen plan, which is Subscriptions::Plan.

But what you couldn't predict was that plans themselves would grow in their function and down the road you're going to add the concept of categories for plans (e.g. monthly vs. annual plans) and that categories are going to be modeled as Subscriptions::Plans::Category.

But wait, if there is a Plans:: namespace... well, shoot, if we knew we were going to have that, we would have called it a Subscriptions::Plans::Plan, right? (Just like subscriptions were Subscriptions::Subscription?)

But we didn't know that namespace was going to exist, so we didn't. And this is why I think a more formulaic approach is to leave the primary subject of a namespace out of it's own namespace. It requires less foresight, or actually no foresight.

So, if you're keeping score, my actual recommended approach would be as follows:

  • Subscription
  • Subscriptions::Plan
  • Subscriptions::Plans::Category

This allows for us to not know how big a piece of functionality a particular model might grow into, but it gives us a clear path for isolating models related to a particular subject when the time comes to do so.

The Simpler Example

The sensibility of this approach is actually even better illustrated if we take a step back at the simpler example and assume no foresight about the subscription system entirely.

Consider someone who, having never implemented a subscription system before, just creates a Subscription model without first considering that there would be other models related to it.

When they realize they're going to need a model to represent the plans people can subscribe to, they should be able to add it naturally as Subscriptions::Plan without feeling like they need to then rename Subscription to Subscriptions::Subscription.

(Inversely, if they do try to rename that model, think of how painful reworking those controllers and views would be. Yikes. No, thanks!)

Drop references to a namespace within that namespace.

If we have a Surveys::Answer model with a belongs_to relationship to Surveys::Question, we would:

  1. Generate the association with the name question, not surveys_questions, (e.g. rails g migration add_question_to_surveys_answers question:references.)
  2. We need to edit the migration so that foreign_key: true is foreign_key: {to_table: 'surveys_questions'}.
  3. We define the relationship as belongs_to :question. (Rails will figure out that it's actually Surveys::Question, but if you run into trouble just add class_name: 'Surveys::Question'.)

We do the same in the situation of a Subscription model with a belongs_to relationship to Subscriptions::Plan. It should be subscription.plan, not subscription.subscriptions_plan.

Drop the namespace when naming instance variables in resource controllers.

The hierarchy of namespaces for the resource controllers will typically match the hierarchy of the namespaces for the models themselves. For example, the Surveys::Answer might be managed from a Surveys::AnswersController or Api::V1::Surveys::AnswersController.

Given that these controllers are in a matching namespace hierarchy, it would be both redundant and annoying to have to refer to them as @surveys_answer or @surveys_answers. Just use @answer and @answers.

It's worth noting that taking this approach might conflict with the assumptions of magical hooks provided by libraries like CanCanCan, but you can work around it, and in my opinion it's worth working around.

Also note that your form helpers will still generate their submission under params[:surveys_answer]. That's OK. Just go with it. 🤙

What about bridge tables for many-to-many associations?

If we have a many-to-many relationship between Page and Tag, do we call
the bridge model PageTag or Pages::PageTag?

I don't know. 🤷‍♂️ I wouldn't be surprised if I started standardizing on the later, but I don't have a strong feeling one way or the other, but we definitely shouldn't call it Pages::Tag.

However, if we have a many-to-many relationship between Pages::Page and Tag, we do call the bridge model Pages::PageTag, not TagPage or PageTag, and definitely not Pages::Tag.

Use shallow nesting in your routes!

In this article we've primarily focused on how to structure things in app/models. However, as a word of warning: Any approach to namespacing models will have ugly, troublesome consequences when it comes to routing and path helpers at some point unless you invoke shallow nesting of routes across the board in config/routes.rb.

I say "at some point" because depending on what you're initially modeling, you may not run into the ugly results right away, but there is another article on this blog that tries to illustrate all the possible relationships between namespaced models and a good number of them will result in ugly routes in the absence of shallow nesting. Inversely, when you're using shallow nesting of routes, all of those examples end up with perfectly logical results.

If you're not familiar with shallowing nesting for routes, don't sweat it! When I first launched Bullet Train, it seemed like around 50% of developers I would speak with hadn't heard of the feature before. However, I'd encourage you to read the shallow nesting documentation and adopt this approach. I'm not being hyperbolic when I say I think it's been the single biggest improvement to my approach building Rails applications since I first started in 2010, and it unlocked a lot of the experimentation and real-world implementation that resulted in this and the other namespacing articles on the Bullet Train blog. Thankfully, using shallow nesting for routes in a new Rails application is as simple as wrapping all your routes in:

shallow do
  # All your resources!
end

Conclusion

Well, that's it! In practice, I've found this to be a really straightforward, systematic approach and I no longer secondguess myself when generating the code for a large domain model.

I'd love to compare notes with anyone. If what you're reading resonates with you or matches the approach you've been taking, please do drop me a line either publicly or via DM on Twitter and let me know. It's really helpful to know!

The same is true if you've read this far but prefer a different approach yourself. I'd love to hear about it!