DEV Community

Mac Siri
Mac Siri

Posted on

How to best organize a Rails app folder?

Aside from all the rudimentary folders, we have a few that are generated by gems we use, and then there's the custom folders we've created. It's not obvious sometime what should go where. We have folders called "labor" and "services" which are for plain-old-ruby-objects; We had some purpose to these patterns but it never really materialized. Now it's just confusing.

app
├── assets
├── controllers
├── dashboards (created by administrate gem)
├── decorators (created by draper gem)
├── fields (created by administrate gem)
├── helpers
├── javascript (created by Webpacker gem)
├── labor [** we created **]
├── lib
├── liquid_tags [** we created **]
├── mailers
├── models
├── observers [** we created **]
├── policies (created by Pundit gem)
├── sanitizers [** we created **]
├── services [** we created **]
├── uploads (created by carrierwave gem)
└── views
Enter fullscreen mode Exit fullscreen mode

Or check it out here on GitHub

How do you organize your Rails app folder? Any and all opinionated suggestion welcomed!

Top comments (12)

Collapse
 
rhymes profile image
rhymes • Edited

I didn't think about it but peaking into other Rails's open source apps might help.

Discourse for example has a services folder with a clear structure and I see that they also centralized async jobs: github.com/discourse/discourse/tre...

Collapse
 
timohermans profile image
Timo

Just signed up to +1 this excellent suggestion, thanks!

Collapse
 
maestromac profile image
Mac Siri

Thanks for this advice rhyme!

Collapse
 
bhserna profile image
Benito Serna • Edited

This is our current app folder...

▾ app/
  ▸ adapters/ (** created by us, for dependencies in our "services" **)
  ▸ assets/
  ▸ controllers/
  ▸ helpers/
  ▸ javascript/
  ▸ jobs/
  ▸ mailers/
  ▸ models/
  ▸ presenters/ (** created by us, but is deprecated... **)
  ▸ serializers/ (** for ActiveModel::Serializers, but is also deprecated... **)
  ▸ services/ (** created by us, here we put all the business logic **)
  ▸ uploaders/ (** for carrierwave **)
  ▸ views/

I think we do things similar to what @andreasklinger says...

move everything into app/models
use namespaces for each distinct domain logic concept
use the root-module as api for that domain logic concept

But instead of using the models folder we use app/services... We use namespaces for each distinct domain logic concept and we use the root-module as the API for each domain concept.

For example if dev.to where structured like our app, maybe in app/services, instead of having something like...

ArticleApiIndexService#get
ArticleCreationService#create!
ArticleWithVideoCreationService#create!

#or

MembershipService#subscribe_customer
MembershipService#update_subscription
MembershipService#unsubscribe_customer
MembershipService#find_subscription
#..

You would have something like...

module Articles
  def self.list_for_api(*args)
  end

  def self.create_text_article(*args)
  end

  def self.create_article_with_video(*args)
  end

  #...
end

module Memberships
  def self.subscribe_customer(*args)
  end

  def self.update_subscription(*args)
  end

  #....
end

And we have as an internal rule/convention not to call anything nested inside each namespace outside of it. So in a controller you could have something like...

def index
  articles = Articles.list_for_api(*args)
  # ...
end

# but not...

def index
  articles = Articles::ListForApi.new(*args).call() 
  # ...
end

This help us to refactor the internals without breaking a lot of things =)

Collapse
 
andreasklinger profile image
Andreas Klinger ✌️️ • Edited

re folders:
i used to use app/services - eg ProductHunt is still structured that way

at some point the separation between a few classes in app/models and app/services just became weird - especially if they work super close together - eg a concern calling a service object

so in newer projects i just use app/models - keep in mind that models stands for classes that model your domain logic - not just persisted state

re namespaces:
i do the same setup as benito w/ the namespaces

usually the logic starts in the method - and when it becomes more than a 3 liner i extract it to some class so it easier to extend

eg

module Campaigns
  extend self
  class ParticipationRequired < StandardError; end
  class InviteRequired < StandardError; end

  # ....

  # simple method
  def allowed_to_join_late?(invite:)
    invite.present? && invite.allowed_to_join_late?
  end

  # more complex stuff
  def flag!(**args)
    Campaigns::Participation::Flag.flag!(**args)
  end```



re api/separation:
like benito i try to keep separation - as in: namespaces dont touch into the deeper parts of other namespaces. controllers neither

the exception are usually rake tasks and sometimes admin backend/stuff
Collapse
 
rhymes profile image
rhymes

@bhserna :

my issue with this style (which I have used until recently and trying to move away from) is that you end up with giant modules that contain methods for everything related to "article", it's not that different from the "fat model" pattern.

A better way would be to group stuff by function, not by object, like they do in the labor folder in a way

Collapse
 
bhserna profile image
Benito Serna • Edited

Hi @rhymes =)

More than a module for everything related to "article" (or active record object).... I think that the term "Domain concept" that Andreas used was more accurate...

I don't think is the same as the "fat model" pattern, because also as Andreas said this modules should be used as the API for a domain concept, but you are not forced to implement the behavior there... you can delegate the behavior to other objects/modules inside... maybe something similar to what they do on the labor folder...

And also another difference with the "fat model" is that these modules should not have any state there. This can help you to move the behavior to other modules in and easier way that when you want to move behavior from an stateful object.

Even though is true that this module can grow to something that you don't want and you should be careful...

Two patterns that we have used are...

  1. Have modules for more "concrete concepts"... for examples we have one module Payments but as the application has a lot of behavior in this concept... for some specific concepts related also to payments, we added some other modules like PaymentsStatus, PaymentProjection, MovementsTable, etc...

  2. Delegate to internal module... For example we have a Projects module that have some behavior to keep a budget... At the beginning this behavior was implemented calling classes just one level deep, but then the behavior started to grow and we move the behavior to a Projects::Budgets but we are still calling the behavior through the Projects module...

module Projects
  def self.get_expenses(*args)
    Budgets.get_expenses(*args)
  end
end

Projects.get_expenses(*args)

... And well this method have worked for us, that is why I wanted to share it, but maybe there are other ways that can be better, but we have not tried yet =)

Thread Thread
 
rhymes profile image
rhymes

Thanks for the explanation! Now I understand better :-)

It's more or less what I ended up doing with service objects/modules and "business objects/modules" but something left me unsatisfied and I want to try a better approach the next time around.

I wonder if a tool like dry-auto_inject could help cleaning up the code.

Collapse
 
vinibrsl profile image
Vinicius Brasil • Edited

Our app folder structure:

app/
├── assets
├── chewy (created by Chewy gem)
├── controllers
├── helpers
├── javascript
├── jobs
├── mailers
├── models
├── searchers (created by us, to handle Elastic Search queries)
├── serializers (created by AMS)
├── services (created by us)
├── uploaders
├── validators (created by us, to handle ActiveRecord custom validators)
├── value_objects (created by us)
├── views
└── workers
Collapse
 
ben profile image
Ben Halpern

@andreasklinger We're finally getting around to doing something about this. I've definitely taken to your past comments on this issue but you're welcome to go into it here as well if you like.

Collapse
 
andreasklinger profile image
Andreas Klinger ✌️️

IMHO:

move everything into app/models
use namespaces for each distinct domain logic concept
use the root-module as api for that domain logic concept

Collapse
 
adrienpoly profile image
Adrien Poly

I usually rename the webpack Javascript fielder to frontend and use it also for my css