One benefit of application frameworks like Ruby on Rails is they provide conventions for organizing your code. ActiveRecord models go in the app/models/ directory. Controllers go in app/controllers/. You get the idea.

Sooner or later, you will write some code that doesn’t fit neatly into one of the conventional locations - background workers, import classes, API calls, etc. Too often I see even experienced developers shove code in lib/ because they don’t know where else to put it. We can do better.

Before we dive in, I would like to point out that the examples in this post should not be considered recommendations. Rather, they were chosen to illustrate the guidelines I use to decide where to put code that doesn’t fit elsewhere. My thinking process can be summarized as followed:

Is this code specific to my application, or could it be be useful to other applications?

In other words, if the code you are writing is specific to the problem domain of your application, a.k.a. your business logic, it should go in app/, but if it is general enough that it could be a candidate for a standalone library or API service, then lib/ is a better choice.

When to choose lib/ over app/

Let’s take an example of importing a CSV file containing customers into your app’s database. First, you create a StandardImport class, that initializes, validates, and saves a Customer model for each CSV row in the file, but you’re not sure where to save this file. It’s not a conventional model or controller and definitely not a view, so you save it to lib/standard_import.rb and call it good - it works, done.

But we can do better. If code references your application’s models directly, then it most likely belongs in the app/ directory. In the import example, StandardImport uses the Customer model directly, so a better choice of file location would be app/imports/standard_import.rb. Unless, the CSV file is in a standard format AND your database schema is in a standard format (does such a thing even exist for DB schemas?), then it is unlikely that you would ever re-use this class in another application.

You can use this same reasoning to decide where to put your presenters, form objects, background workers, and any other kinds of objects that don’t fit neatly into Rails conventions.

Note: because app/imports/ is not a conventional Rails directory, you will want to update the autoload paths. To do this, add the following line to config/application.rb.

    config.autoload_paths << root.join(‘app/imports’)

When to choose app/ over lib/

Now, let’s look at an example where we might put code in app/ that really belongs in lib/.

As your application becomes more popular, you get many requests for a CRM import, so you create a CrmImport class and place the code in app/imports/crm_import.rb. In the process, you notice that CrmImport and StandardImport share a lot of code, so you refactor the classes to remove the duplication and create a generic ImportMapper class. For now, you save it to app/imports/import_mapper.rb, but is this really the best place for the ImportMapper code?

The ImportMapper class solves the generic problem of mapping an object in one format to an object in a different format. It doesn’t care what application those objects belongs to, it only provides a convenient way to convert from one to another. It doesn’t even have to be used in an import context. As you continue extending your application, you find that the ImportMapper is also useful for mapping request parameters and API responses to your ActiveRecord models.

Now the ImportMapper class is starting to sound like a generic library. Perhaps you are already thinking it would make a good candidate for a Ruby gem because you think others will find it useful, too. At this point, we can confidently rename the class to ObjectMapper and move it to lib/object_mapper.rb.

Summary

Hopefully, this helps you think through how to better organize files in your projects. If you are new to Rails, maybe this is the first time you’ve seen how to go beyond the conventions, and if you’re a veteran, maybe you have guidelines of your own. Wherever you are in your journey, I would love to hear from you in the comments :-)