Setting Up Business Logic with DCI in Rails
Tags: ruby, rails
Published: 2023-06-21 14:00:00

In our last post, we examined the most common ways to organize business logic in Ruby on Rails. They all have advantages and drawbacks, and essentially, most do not leverage the full power of Object Oriented Programming in Ruby.

This time, we will introduce another alternative that more naturally fits the mental models we apply when reasoning about the behavior of our applications: DCI.

## Enter DCI (Data, Context, Interaction) for Rails

DCI is an often overlooked paradigm that can circumvent a lot of the issues outlined in the last post, while still adhering to MVC.

More importantly, though, it's a code architecture style that simultaneously lets us consider an application as a whole and avoids introducing objects with unclear responsibilities.

Without further ado, here's the [definition of DCI from Wikipedia](https://en.wikipedia.org/wiki/Data,_context_and_interaction):

> The paradigm separates the **domain model** (data) from **use cases** (context) and **roles** that objects play (interaction). DCI is **complementary** to model–view–controller (MVC). MVC as a pattern language is still used to separate the data and its processing from presentation.

One of the main objectives of DCI is to improve a developer's understanding of _system-level state and behavior_ by putting their code representation into different modules:

- slowly changing domain knowledge (_what a system is_)
- rapidly changing system behavior (_what a system does_)

Moreover, it tries to simplify the mental models of an application by grouping them into _use cases_.

Let's break this down into its individual parts: data, context, and interaction.

### Data

In DCI terms, _Data_ encompasses the static, descriptive parts of what we call the _Model_ in MVC. It **explicitly lacks functionality** that involves any interaction with other objects. In other words, it's devoid of _business logic_.

```rb
class BankAccount
  attr_reader :balance

  def increase(by)
    @balance += by
  end

  def decrease(by)
    @balance -= by
  end
end
```

Consider the simple `BankAccount` class above: it allows you to query the account balance, and increase or decrease it. But there is no such concept as a _transfer_ to another account.

'Wait a second!' I hear you say! Isn't that a description of an [_anemic model_](https://www.martinfowler.com/bliki/AnemicDomainModel.html)? And isn't that the most horrific anti-pattern of all time 👻? Bear with me for a moment.

### Context

The point of DCI is not to strip models of any domain logic, but to dynamically attach it when it's needed, and tear it down afterward. _Context_ realizes this.

A context is responsible for identifying a _use case_ and mapping data objects onto the _roles_ that those play.

The nice thing, by the way, is that they align nicely with our _mental models_ of everyday processes. People don't carry every role around with them all the time either.

### Real-World Examples

A school classroom, for example, is composed of people, but some are _teachers_ and some _students_.

Similarly, some people are _passengers_ on public transport, and some are _conductors_. Some even play multiple roles at once that may change over time — e.g.:

- I'm a _passenger_.
- If it's a long ride, I might also assume the role of _book reader_.
- Suppose someone calls me on the phone. I'm simultaneously a _conversation participant_.
- If I travel with my daughter, I'm also her _dad_ and have to look after her.

And so on.

### Back to Our `BankAccount` Example in Rails

Let's continue with the `BankAccount` example from above, and expand it with a requirement to transfer money from one person to another. Consider the following module, which just defines a method to transfer the money:

```rb
module MoneyTransferring
  def transfer_money_to(destination:, amount:)
    self.decrease amount
    destination.increase amount
  end
end
```

The key notion in this snippet is the reference to `self`, as we shall see in a moment.

The beauty of applying this pattern to Ruby is the ability to inject modules at _run time_. A _context_ can then map _source_ and _destination_ roles:

```rb
class Transfer
  delegate :transfer_money_to, to: :source

  def initialize(source:)
    @source = source
    @source.include(MoneyTransferring)
  end
end
```

So, now `BankAccount` is equipped with the ability to `transfer_money_to` another account in the `Transfer` context.

### Interaction

The final part of DCI — interaction — comprises everything _the system does_.

It is here that the _use cases_ of an application are enacted through _triggers_:

```rb
Transfer.new(source: source_account)
  .transfer_money_to(destination: destination_account, amount: 1_000)
```

The _source_ and _destination_ roles are mapped to their respective domain models, and a transfer takes place. In a typical Rails app, this would happen in a controller or a job — sometimes even in model callbacks.

A critical constraint of DCI is that these bindings are guaranteed to be in place **only at run time**. In other words, the `Transfer` object will be picked up by the garbage collector afterward, and no trace of the mapped roles remains with the domain models.

If you flip this around, this ensures that DCI roles are _generic_, making them both easier to reason about and test. In other words, the `Transfer` context makes no assumptions about the _kind of_ objects its roles are mapped to. It only expects `increase`/`decrease` methods. **The fact that they are `BankAccounts` with an attached state is irrelevant!** They could equally be other types of objects (e.g., `Wallets`/`StockPortfolios`/`MoneyBox`). The context does not care. Only through its enactment in a certain use case are the roles associated with certain types. As the snippet above shows, it's succinct and readable.

## Case Study Using DCI in a Rails Application

I want to conclude this article with an example from a real-world app where I used DCI to organize parts of the business logic. I will attempt to show how regular Rails MVC can _enact a DCI use case_. Note that I'm using Jim Gay's [surrounded gem](https://github.com/saturnflyer/surrounded) to strip away some of the boilerplate.

Here's a `Checkout` context that includes methods to create and fulfill a `Stripe::Checkout:Session` object:

```rb
# app/contexts/checkout.rb
class Checkout
  # ...

  role :payable do
    def create_session
      Stripe::Checkout::Session.create({
        line_items: line_items, # provided by model
        metadata: {
          gid: to_gid.to_s
        },
        success_url: polymorphic_url(self)
        # ...
      })
    end

    def fulfill
      # ...
    end
  end
end
```

Imagine that the class method `role :payable` is just a wrapper around the manual `.include(SomeModule)` we did above; `create_session` and `fulfill` are called the **RoleMethods** of this context. Note that in this case, the `create_session` method only relies on `self`, `to_gid` (present on any `ActiveRecord::Base` subclass), and a `line_items` accessor.

We can now write a test to enact this context:

```rb
class CheckoutTest < ActiveSupport::TestCase
  # VCR/Stub setup omitted

  test "created stripe checkout session includes gid in metadata" do
    @quote = quotes(:accepted_quote)

    session = Checkout.new(payable: @quote).create_session

    assert_equal @quote, GlobalID::Locator.locate(session.metadata.gid)
  end
end
```

This looks good! Now let's look at two separate use cases for this context.

### Use Case 1: Quote Checkout

In my app, a `Quote` is a bespoke offer to a certain customer. It typically contains only one line item, which Stripe will use to create a price:

```rb
# model
class Quote
  def line_items
    [{price: price_id, quantity: 1}] # Stripe::Price
  end
end
```

Now a Stripe Checkout session can be created in a controller as follows:

```rb
# controller
@checkout_session = Checkout.new(payable: @quote).checkout_session
```

This is then used in the view to send the customer to a Stripe Checkout form via a simple link:

```erb
<!-- view -->
<%= link_to @checkout_session.url, target: "_top", class: "btn btn-primary", id: dom_id(@quote, :checkout_button) do %>
  <%= t(".checkout") %>
<% end %>
```

### Use Case 2: Review Checkout

Another payable in my app is a `Review`, which contains many chapters, each with its own line item:

```rb
# model
class Review
  def line_items
    chapters.map do |chapter|
      {price: chapter.price_id, quantity: 1} # Stripe::Price
    end
  end
end
```

Apart from exchanging the `payable`, the code for enacting the checkout use case stays exactly the same:

```rb
# controller
@checkout_session = Checkout.new(payable: @review).create_session
```

```erb
<!-- view -->
<%= link_to @checkout_session.url, target: "_top", class: "btn btn-primary", id: dom_id(@review, :checkout_button) do %>
  <%= t(".checkout") %>
<% end %>
```

## Takeaways

DCI certainly isn't a grab-all solution to code organization, but compared to some other approaches, it feels like a natural inhabitant of a Rails app's ecosystem. What enthralls me the most is that it provides a clear structure for separating descriptive structure ("what the app _is_") from behavior ("what the app _does_") without compromising the [SOLID principles](https://en.wikipedia.org/wiki/SOLID) for OOP design. This separation makes it a breeze to refactor key parts of such an app.

The actual business logic design also feels more streamlined because DCI attempts to closely reflect our mental models of the software we build.

That said, like any design pattern or paradigm out there, it's not a hammer that fits every nail. You might find that this separation of behavior from data is something that diminishes code cohesion in your app, for example.

If you want to try it on for size, I recommend using DCI for integrating third-party APIs, or with fringe concerns that don't directly touch your app's core functionality. That's because those areas typically don't change very often and are thus ideal playgrounds for experimenting with new tools.

## Wrapping Up and References

In part one of this two-part series, we examined common approaches for building business logic in your Rails application, including fat models, service objects, jobs, and event sourcing.

In this second and final part, we turned our attention to DCI specifically, exploring each individual part: Data, Context, and Interaction. We showed how to use the DCI paradigm in a real-world Rails application.

Happy coding!

References:

- [Clean Ruby eBook](http://www.clean-ruby.com/)
- [Surrounded Gem](https://github.com/saturnflyer/surrounded)

**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)!**