Organize Business Logic in Your Ruby on Rails Application
Tags: ruby, rails
Published: 2023-05-17 11:16:02

With its strong emphasis on _convention over configuration_, Ruby on Rails has counteracted many architectural considerations that caused [bikeshedding](https://en.wikipedia.org/wiki/Law_of_triviality) when building web applications.

Still, one area that has continuously piqued developers' interest is how to handle _business logic_, i.e., code that epitomizes "what an app does." Another way to phrase this question is: _Where do we put all the transactional code?_

In this first part of a two-part series, we'll explore the most well-known methods to organize your business logic in a Ruby on Rails application.

Let's get going!

## Where to Put Transactional Code in Rails

There has been an ongoing debate about this topic in the Rails community, with two extremes being advocated for, and a couple of intermediate solutions.

On one hand, there's the **"Golden Path"**, which fully embraces MVC and is most prominently followed by [37Signals](https://dev.37signals.com/vanilla-rails-is-plenty/).

On the other hand, there is **Domain Driven Design (DDD)**, practiced by companies like [Shopify](https://shopify.engineering/shopify-made-patterns-in-our-rails-apps), and consultancies like [Arkency](https://products.arkency.com/domain-driven-rails/).

These alternative approaches look pretty heavy-handed. The one you should choose depends very much on the following:

- how many business domains your app spans (DDD)
- organizational structure
- app architecture/infrastructure (microservices/monolith, k8s/cloud, etc.)

Let's take a closer look at some of the familiar alternatives, and their pros and cons, starting with fat models.

### Fat Models

Fat models are models equipped with methods for processing their data, possibly resulting in side effects and updating other records. Consider this example, simplified from [Jorge Manrubia's post about rich models](https://dev.37signals.com/vanilla-rails-is-plenty/):

```rb
# app/models/recording
class Recording < ApplicationRecord
  def copy_to(bucket, parent: nil)
    copies.create! destination_bucket: bucket, destination_parent: parent
  end
end
```

One problem with fat models is that unless you are really disciplined in your code hygiene, they can tend towards an assortment of code smells:

- **Feature Envy**: Models reaching out to other models, either to query their internals (a _"tell, don't ask"_ violation) or to actually mutate them, are a red flag 🚩. To achieve low coupling, objects should be confident just _sending messages_ to other objects without querying them for the results.
- **Single Responsibility Principle Violations**: Models encoding interactions with other models may break the rule of single responsibility. Keep rigorously asking yourself if the code you are adding to a class really belongs to the _representation of the object_ it describes.
- **Logic in Callbacks**: An enduring controversy, model callbacks are really a double-edged sword. A powerful tool, they can simplify a lot of imperative code, but can also hide complexity and lead to bugs that are hard to track down. Here is a rule of thumb you can follow: only use model callbacks to prepare or post-process `self`. Never trigger any jobs, mailers, or other services from them.
- **Tendency towards God Objects**: Rich models act as _attractors_ for functionality. Wait a few development cycles, and I'll take any bet there'll be at least one model accumulating a few dozen methods. Even splitting up such an object into model concerns merely covers up the mess 🌶️.

The points listed above can be summarized as a question of _perspective_. To paraphrase [Jim Gay](http://www.clean-ruby.com), is a look from _within an individual model_ actually a good vantage point to design _what your app does_?

This has been picked up by a recent advance in Rails development: namely, service objects.

### Rails Service Objects

Really another title for the [Command Pattern](https://refactoring.guru/design-patterns/command), service objects encapsulate a "unit of work", or "action", that usually involves several steps of transactional logic. Following the above example, you would typically formulate something like this:

```ruby
# app/services/recording_copier.rb
class RecordingCopier
  def call(source, destination)
    source.copies.create! destination_bucket: destination
  end
end
```

There are multiple problematic facets of this pattern, but the focal point is:

**This object does not encapsulate an instance state.**

This is a sign that service objects do not actually describe any concept of our domain, as [Jason Swett has noticed](https://www.codewithjason.com/rails-service-objects/).

Others - like [Avdi Grimm](https://avdi.codes/service-objects/) - have observed that service objects run counter to the accepted practice of representing your domain _objects_ with _nouns_, and sends _messages_ to these objects with _verbs_. Classes named using the nominalization of a verb indicate that you have identified a _message_ you want to send but can't come up with a _receiver_, i.e., a matching _role_ to send it to. Objects with such fuzzy responsibilities can be the hardest to refactor.

First and foremost, though, I always found this concept a bit confusing, because Rails already has a built-in primitive for this: jobs.

### Jobs in Rails

In my consulting practice, I've observed that jobs are generally underused because their limits and requirements can seem daunting. Let's first rewrite our example as a job:

```ruby
# app/jobs/copy_record_job.rb
class CopyRecordJob < ApplicationJob
  def perform(source, destination)
    source.copies.create! destination_bucket: destination
  end
end
```

As you can see, there's almost no syntactical difference between a job and a service object. What, then, sets them apart?

A couple of things, actually:

- The code above isn't **idempotent**. If you run it twice, it will create two copies, which is probably not what you intended. Why is this important? Because most backend job processors like [Sidekiq](https://sidekiq.org/) don't make any guarantees that your jobs will run _exactly_ once.
- Jobs cannot return a value or indicate when they are done out of the box (although you can do this manually, via callbacks, for example).

There are several workarounds for this, like the magnificent [Acidic Job](https://github.com/fractaledmind/acidic_job) gem or Sidekiq Pro/Enterprise features around [enhanced reliability](https://github.com/sidekiq/sidekiq/wiki/Pro-Reliability-Client) and [unique jobs](https://github.com/sidekiq/sidekiq/wiki/Ent-Unique-Jobs). Still, if they occur, bugs related to missing jobs and/or job idempotency are hard to track down and even harder to fix.

### Event Sourcing

> Event Sourcing ensures that all **changes to application state** are stored as a **sequence of events**. Not just can we query these events, we can also use the event log to **reconstruct past states.**

_Source: [Event Sourcing by Martin Fowler](https://martinfowler.com/eaaDev/EventSourcing.html)_

Essentially, event sourcing boils down to a publish/subscribe algorithm with integrated versioning. It's a high-level Domain Driven Design concept that I will not discuss in detail here.

That's not to say it's not an interesting pattern. You should use it if you have advanced reporting requirements, for example. If you want to learn more about it, look at [Rails Event Store](https://railseventstore.org/).

## Up Next: DCI (Data, Context, Interaction) for Rails

We looked at a few of the most popular approaches to organize your business logic in a Ruby on Rails application: fat models, service objects, jobs, and (briefly) event sourcing.

In the next and final part of this series, we will look at a convenient alternative called _DCI (Data, Context, Interaction)_. DCI caters particularly well to the mental models we employ as engineers when we reason about application behavior.

Until then, happy coding!

**P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, [subscribe to our Ruby Magic newsletter and never miss a single post](https://blog.appsignal.com/ruby-magic)!**