Porting Phoenix contexts to Rails

I've recently started client work on a new Rails 7 application and while I'm impressed always impressed by the developer experience that the Rails community has achieved, one thing that I instantly missed from my recent work with Phoenix was the idea of contexts.

What are Phoenix contexts

If you're new to Phoenix, here's a short explanation: Like Rails, Phoenix has an opinion about how your code should be structured.

First of all, Phoenix separates between HTTP-related code that lives in lib/myapp_web and your actual application logic that lives in lib/myapp . Simply put: your controllers and views go into the lib/myapp_web directory and thus away from everything else.

On top of that, Phoenix goes on and talks about contexts, which fundamentally means that your code should be organized around the problem domain: Related code should be close together and boundaries between unrelated code should be explicit.

There's a lot more that can be said about this idea and I feel like I should write a separate post about why it's so important to me, but I'll focus on how to implement it in Rails for this article.

Phoenix Contexts in Rails land

The first step is easy: We just create an app/contexts directory and put code into a new module there. Our controller then calls the module.

# app/contexts/blog.rb
module Blog
  module_function

  def publish_article(article_id)
    Article.find!(article_id).update(published: true)
    # Notify your subscribers here etc.
  end
end

# app/controllers/admin/articles_controller.rb
class Admin::ArticlesController < ApplicationController
  def publish
    if Blog.publish_article(params[:article_id])
      # Success!
    else
      # Ouch, render an error.
    end
  end
end

Fairly straightforward. But our models still live in the app/models directory and outside of the context, so let's fix that.

# app/contexts/blog/article.rb
class Blog::Article < ApplicationRecord
end

Nice! At this point, Rails still hooks these models up to the articles table - no mention of our context in that table name! Let's fix this to avoid any conflicts if the same model name is used in more than one context:

# app/contexts/blog.rb
module Blog
  module_function

  def table_name_prefix
    'blog_'
  end

  # ...
end

From that point on, all models in the Blog module will have their table names prefixed:

irb(main):015:0> Blog::Article.table_name
=> "blog_articles"

If you followed along with your actual code, you'll need a migration to rename your existing table.

And that's it! For now, I'm quite happy with how this turned out. If you do this, I'd be happy to hear about your experience. I'm very interested in seeing this applied to a larger code base.