Pimp your Rails Application
Tags: ruby, rails, architecture
Published: 2020-07-01 12:36:24

---
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/o6f7ycx3ip8s9vulz30o.jpg
---

Hi DEV community!

Today I want to share something with you folks I've been waiting for weeks now but was to busy to write 😁

It's about the question "How to structure rails applications".

Finding a good and maintainable structure for large rails applications can be challenging. When you don't have a good approach, you'll end up with fat controllers and even fatter models. You keep on coding and at some point you'll lose the overview and have to maintain a spaghetti (code) monster.

Rails already comes with some features to extract code from your controllers, models and views by using concerns, helpers and so on. This is nice but far not enough.

Over the years I used a bunch of different approaches in a bunch of different rails apps. Starting with simple lib classes, tried Trailblazer, different gems and stuff. Finally I found a robust solution how to structure my apps to keep them clean, DRY, testable and maintainable: UseCases and Services (and Behaviors).

Recently I've took alle the code which I have grown over the last 2 years and published it as a gem. Think of it as the framework and support for UseCases, Services and Behaviors. 

**[RailsUseCase gem](https://github.com/phortx/rails-use-case)**

Enough talked, let's dip into my take on UseCases, Services and how this gem helps you to write them without boilerplate code.

For that purpose I distinguish between two types of code: low-level technical code and high-level business logic. Services are for low-level code. UseCases for business logic. Pretty simple.

Now let's see what that even means ...


## Services for Low Level Code

Services are simple ruby classes which contain non-domain logic for low level, technical stuff. For example downloading a file from a FTP server, communication with an API, CSV generation, attaching a webcam image to a record, whatever. 

Services are placed in the `app/services` directory and should be named like `csv_generation_service.rb`.

A example service could look like this:

```ruby
class PDFGenerationService < Rails::Service
  attr_reader :pdf_template, :values

  # Constructor.
  def initialize
    super 'pdf_generation'
    prepare
    validate_libreoffice
  end


  # Entry point.
  #
  # @param [PdfTemplate] pdf_template PdfTemplate record.
  # @param [Hash<String, String>] values Mapping of variables to their values.
  #
  # @returns [String] Path to PDF file.
  def call(pdf_template, values = {})
    @pdf_template = pdf_template
    @values = prepare_variable_values(values)

    write_odt_file
    replace_variables
    generate_pdf

    @pdf_file_path
  ensure
    delete_tempfile
  end
end
```

The `Rails::Service` class comes with some handy features to make your life easier:

- Call style invocation: `PDFGenerationService.(some, params)` which is the same as `PDFGenerationService.call(some, params)`
- They are configurable and automatically load their configuration both from `config/services/shared.yml` and `config/services/[service_name].yml`. You can access the config as a hash via calling `config` within the service.
- Each service writes it's own log file to `log/services/[service_name].log`. You can access the logger via `logger` within the service.

Services replace what was usually done via lib files. They have a clear structure, are reusable, DRY and are easy to test.


## UseCases for Business Logic

While the service contains low-level non-business logic, UseCases are for the opposite: high-level business logic and they replace fat controllers. A UseCase takes params and has a outcome, which is successfully or failed. Examples are: Place an item in the cart, create a new user or delete a comment.

Use Cases should be placed in the `app/use_cases` directory and the file and class name should start with a verb like `create_blog_post.rb`. I also highly recommend to namespace all use cases with the model module like `blog_posts/create.rb`.

Example UseCase for creating a blog post:

```ruby
module BlogPosts
  class Create < Rails::UseCase
    attr_accessor :title, :content, :author, :skip_notifications

    validates :title, presence: true
    validates :content, presence: true
    validates :author, presence: true

    step :build_post
    step :save!
    step :notify_subscribers, unless: -> { skip_notifications }


    private def build_post
      @record = BlogPost.new(
        title: title,
        content: content,
        created_by: author,
        type: :default
      )
    end

    private def notify_subscribers
      # ... send some mails ...
    end
  end
end
```

Let's take a look on the details: 
- We can define the params of the UseCase by defining `attr_accessors`.
- The params should always passed as hash and are automatically assigned to instance variables.
- We can define validations for those params via ActiveRecord Validations. These are automatically executed when calling the UseCase
- The logic of the UseCase is defined by `step`s. Each step calls the method with the same name. They can be skipped via `if` or `unless`
- Steps are executed in the defined order. Only when a step succeeds (returns `true`) the next step will be executed. Steps can be skipped via `if` or `unless`.
- The UseCase should assign the main record which is handled within the Case to `@record`.
- Calling `save!` without argument will try to save that record or raises an exception. Also the `@record` will automatically passed into the outcome.
- raising a `UseCase::Error` immediately stops the UseCase and saves the exception in the outcome.

Calling a UseCase looks like this:

```ruby
outcome = BlogPost::Create.perform(
  title: 'Super Awesome Stuff!',
  content: 'Lorem Ipsum Dolor Sit Amet',
  created_by: current_user,
  skip_notifications: false
)

puts outcome.inspect
# => {
#   success: true,
#   record: BlogPost(...)
#   errors: [],
#   exception: nil
# }
```

- `outcome.success?` Tells whether the UseCase was successful.
- `outcome.record` Contains the record which was assigned to `@record`.
- `outcome.errors` Contain validation messages when the validation failed.
- `outcome.exception` Contains the `UseCase::Error` exception when any was raised.

UseCases may call other UseCases and use Services.

Each controller action or GraphQL mutation should do nothing than calling a UseCase and handle the outcome. This way you have packed your business logic in highly reusable, robust, elegant and clean ruby classes.



## Behaviors

When working with UseCases you will probably have duplicated code at some point. Behaviors allow you to share code between UseCases.

Behaviors should be placed in the `app/behaviors` directory and the file and module name should named in a way it can be prefixed with `with` and should end with `behavior`, like `current_user_behavior.rb` (read: `with current user`).

To use a Behavior in a UseCase, use the `with` directive, like `with CurrentUserBehavior`.

Example Behavior for accessing the current user of [AuthLogic](https://github.com/binarylogic/authlogic):

```ruby
module CurrentUserBehavior
  private def session
    UserSession.find
  end

  private def current_user
    UserSession.find&.record
  end
end
```

Example usage:

```ruby
module BlogPosts
  class Create < Rails::UseCase
    with CurrentUserBehavior

     # ...

    private def build_post
      @record = BlogPost.new(
        title: title,
        content: content,
        created_by: current_user,
        type: :default
      )
    end

    # ...
  end
end
```

This way you have an elegant way to share logic between your UseCases to keep them as small as possible.


I hope I could explain the advantages of using these classes to structure a rails application and would be happy when this helps you to improve your app. Please give feedback for the gem and don't forget to star the project 😉


Thanks for the read. Have a nice day and keep coding! ❤️ 👨‍💻

Header image made by [Safar Safarov @ Unsplash](https://unsplash.com/photos/MSN8TFhJ0is)