---
title: 'Advanced Ruby: The chainable request pattern'
teaser: 'Make your transactional web requests more maintainable with The Chainable
  Request Pattern.

  '
tags: ruby,error handling,api,distributed systems,fault tolerance,rails
author: Thiago Araújo Silva
published_on: 2024-05-06
---

In [the previous article] of my series on error handling and fault
tolerance, we discussed how to run external API requests in a
transaction while handling errors and making it fault-tolerant. That
article is not a requirement to understand this one, so read on in
either case!

Here, we will explore some means of abstraction for the transactional
pattern. You will learn how to encapsulate API requests for use in an
external transaction, including:

- Automatic rollback on error;
- Support for running custom code in between requests;
- Chain multiple API requests with automatic rollback support;
- Return a chained result of all API requests.

Let's call it the "Chainable Request Pattern".

## The request object

The first step is to wrap the API request in a class. The
code must return a result object with the outcome:

```rb
Result = Struct.new(:status, :error_code, :id, keyword_init: true) do
  def success?
    status == :ok
  end

  def error?
    !success?
  end
end

class LineItemRequest
  BASE_URL = "https://app.moderntreasury.com/api/"
  HEADERS = {"Content-Type" => "application/json"}.freeze

  def self.execute(params)
    response = client.post("ledger_transactions", JSON.dump(params))

    if response.status == 200
      Result.new(status: :ok, id: response.body["id"])
    else
      Result.new(status: :error, error_code: response.status)
    end
  end

  private_class_method def self.client
    Faraday.new(url: BASE_URL, headers: HEADERS) do |client|
      org_id = Configuration.organization_id
      api_key = Configuration.api_key

      client.response :json
      client.request :authorization, :basic, org_id, api_key
    end
  end
end
```

The result object must return whether the request was successful or
not.

## The request runner

The role of the request runner is to execute a request object and
optionally run arbitrary code on success. Let's see how it could be
implemented:

```rb
class RequestRunner
  def self.execute(request, params)
    result = request.execute(params)

    if block_given? && result.success?
      yield result
    end

    result
  end
end
```

Using the request object is straightforward:

```rb
params = {
  description: "Scientific Calculator",
  status: "pending",
  ledger_entries: [
    {
      amount: 10_00,
      direction: "debit",
      ledger_account_id: "368feff6-fe48-44f0-95de-50ee1f2d1e50",
    },
    {
      amount: 10_00,
      direction: "credit",
      ledger_account_id: "eec76db0-b537-47bf-a28d-3aa397f35f69",
    },
  ]
}

RequestRunner.execute LineItemRequest, params do |result|
  puts "Success! ID: #{result.id}"
end
```

## Rollbacks

If you are asking, "How can the request runner be helpful? It seems to
be a thin wrapper that provides no value", that is a great question!
And the preliminar answer is **rollbacks**. The request runner runs a
request and executes a block that optionally runs dependent code. If
the dependent code fails, the request runner should automatically roll
back the request.

The first step to implementing that is to write a `rollback` method in
our request object:

```ruby
class LineItemRequest
  # ...

  def self.execute(params)
    # Code for 'execute' goes here
  end

  def self.rollback(result, _params)
    return if result.nil?

    # Archiving is how we roll back our particular operation
    # in the Modern Treasury API.
    patch_params = {status: "archived"}
    client.patch "ledger_transactions/#{result.id}", JSON.dump(patch_params)
  end

  # ...
end
```

<aside class="info">
  <p>
    Our implementation example doesn't use <code>params</code>, so we
    simply prepend an underline to the variable (<code>_params</code>)
    to hint that it's not used.
  </p>
</aside>

If you think this looks familiar, you’re on point, as `LineItemRequest`
implements the [Command pattern].

So far, every request object must implement the following interface:

- `execute(params)` - Returns a Struct result that responds to success?.
- `rollback(result, params)` - Returns void. Don't care for the return value.

Second, we need to hook up `rollback` to our request runner:

```ruby
class RequestRunner
  def self.execute(request, params)
    result = request.execute(params)

    if block_given? && result.success?
      begin
        yield result
      rescue => e
        request.rollback(result, params)
        raise e
      end
    end

    result
  end
end
```

And we do that through an exception handler because we want to revert
our successful request if an exception is raised. After rollback, we
still need to reraise the exception to signal an error to the
application.

<aside class="info">
  <p>
    Why not roll back when the result is an
    error? Because it means the request failed, so its side-effects were
    not applied. We don't need to roll back a failed request.
    Exceptionally, a timeout could occur, and the request could still
    make it through, but we wouldn't get a result with an ID that would
    allow rolling back.
  </p>
</aside>

Now, let's execute the request and try to save an Active Record
reference:

```ruby
RequestRunner.execute LineItemRequest, params do result
  OrderLineItem.create! external_id: result.id
end
```

In Rails, `create!` raises an exception on error. If that ever
happens, our request will automatically roll back. Neat!

## Chaining dependent requests

Do you recall that the role of the success block is to run dependent
code with rollback support? What if that code is another request?
Would it work? Yes!

However, chaining requests poses an exciting challenge: the caller
needs to get the chained results of various requests. Managing local
variables and overriding them in our success blocks would achieve
that, but it's not practical:

```ruby
result_1 = nil
result_2 = nil

RequestRunner.call ... do |r1|
  result_1 = r1

  RequestRunner.call ... do |r2|
    result_2 = r2
  end
end

final_result = [result_1, result_2]
```

Instead, let's change the request runner to do that work behind the
scenes and chain the results in a linked list. By wishful thinking,
the API we want to achieve is:

```ruby
# Different types of request can be chained, not just a single type
result = RequestRunner.execute LineItemRequest, params_1 do |_result_1|
  RequestRunner.execute LineItemRequest, params_2 do |_result_2|
    RequestRunner.execute LineItemRequest, params_3
  end
end

result # Result of the first request
result.next_result # Result of the second request
result.next_result.next_result # Result of the third request
```

Also, if the second or third request fails, the first result should be
an error; after all, it's an _all or nothing_ transaction.

```ruby
# Should always return false on error throughout the chain
first_result.success?
```

Finally, if the second request fails, the chain must halt and not run
the third request:

```ruby
result.success? # false
result.next_result.success? # false
result.next_result.next_result # nil. Third request does not run.
```

Let's see how we can achieve that. The first step is to change the
request runner to chain the previous result with the next result
in case of success:

```ruby
class RequestRunner
  def self.execute(request, params)
    result = request.execute(params)

    if block_given? && result.success?
      begin
        next_result = yield(result)
      rescue => e
        request.rollback(result, params)
        raise e
      end

      if next_result.respond_to?(:chain_result)
        result = result.chain_result(next_result)

        if next_result.error?
          request.rollback(result, params)
        end
      end
    end

    result
  end
end
```

We only call `chain_result` if the current result is successful;
otherwise, we return the current result unchanged.

<aside class="info">
  <p>
    We are naming our method <code>chain_result</code> to avoid overriding
    <code>Struct</code>'s <code>Enumerable#chain</code>.
  </p>
</aside>

We are also rolling back the current request if the next result is an
error, which is a great companion to the exception handler and makes
our code work per our intended design. Oh, and if you are asking if
that works recursively for every chained request, the answer is yes!
This will be clear down below, so read on.

And, of course, we need to implement `chain_result` in our result
object. Because we want the "chain" ability in requests with different
requirements, we need a reusable module:

```ruby
module ResultChainable
  attr_reader :next_result

  def initialize(next_result: nil, **attrs)
    super(**attrs)
    @next_result = next_result
  end

  def chain_result(next_result)
    attrs = {
      status: next_result.status,
      next_result: next_result
    }
    self.class.new(**to_h, **attrs)
  end
end
```

Note that `ResultChainable` requires the result object to respond to
`status` and `to_h` (which comes free with `Struct` objects). The
status of the chained result must become the status of the next result
because one error should make everything fail.

Now, let's include the module in our result class:

```ruby
Result = Struct.new(:status, :error_code, :id, keyword_init: true) do
  include ResultChainable

  def success?
    status == :ok
  end

  def error?
    !success?
  end
end
```

And try it out:

```ruby
result_1 = Result.new(status: :ok, id: "result-1-id")
result_2 = Result.new(status: :ok, id: "result-2-id")
result_3 = Result.new(status: :error, error_code: "101")

chained_result = result_1.chain_result(result_2.chain_result(result_3))

chained_result.status # :error, not :ok
chained_result.id # "result-1-id"
chained_result.success? # false, not true

chained_result.next_result.status # :error, not :ok
chained_result.next_result.id # "result-2-id"
chained_result.next_result.success? # true

chained_result.next_result.next_result.status # :error
chained_result.next_result.next_result.success? # false
chained_result.next_result.next_result.error_code # "101"
chained_result.next_result.next_result.next_result # nil
```

This works as expected! Let's review our request chaining code:

```rb
chained_result = RequestRunner.execute LineItemRequest, params_1 do |_result_1|
  RequestRunner.execute LineItemRequest, params_2 do |_result_2|
    RequestRunner.execute LineItemRequest, params_3 do |_result_3|
      # ...
    end
  end
end
```

With our code so far, the results are chained in a [fold right]
fashion. For example, assuming that the three above requests are
indeed run, their three results would get implicitly folded:

```rb
# This happens behind the scenes
chained_result = result_1.chain_result(result_2.chain_result(result_3))
```

Note that you can still run custom code before running the next
request:

```ruby
result = RequestRunner.execute LineItemRequest, params_1 do
  # Can still run arbitrary code here

  RequestRunner.execute LineItemRequest, params_2 do
    # Can still run arbitrary code here

    RequestRunner.execute LineItemRequest, params_3
  end
end
```

Let's imagine a few scenarios to form a mental picture of how this all
works.

**Scenario 1**: An exception is thrown in the success block of the third request.
**What happens**: Requests 1 and 2 roll back. Chained result is error.

**Scenario 2**: Request 1 succeeds, request 2 returns a non-200 response.
**What happens**: Requests 1 rolls back. Request 3 does not run. Chained result is error.

**Scenario 3**: Requests 1 and 2 succeed, request 3 returns a non-200 response.
**What happens**: Requests 1 and 2 roll back. Chained result is error.

**Scenario 4**: All requests succeed, no exceptions thrown.
**What happens**: No rollbacks. Chained result is success.

## Flattening the chain

If you can, you should avoid running too many dependent requests. If
you are running just a few requests, though, the nesting imposed by
the block structure should not be a big deal. But what if you _need_
to run more than a few requests? Then, unnesting the structure should
improve clarity and readability.

It also formalizes the overall pattern, which comes with a benefit
regardless if you are running just a few requests or more than a few.
Oh, and you can also run requests dynamically with that!

Let's imagine an API where we could flatten our requests:

```ruby
on_line_item_success = -> { OrderLineItem.create!(external_id: _1.id) }

array_of_arguments = [
  [LineItemRequest, params_1, on_line_item_success],
  [OrderRequest, params_2],
]

result = TransactionalRequestRunner.execute(array_of_arguments)

# Do something with the result, or not
```

`array_of_arguments` is a matrix that has the arguments of each
request. For each request, we have the request class, the params, and
an optional block to run on the success of that request where we don't
worry about request chaining because `TransactionalRequestRunner` will
already do it for us.

Now let's write the code to run a batch of requests in a transaction:

```ruby
class TransactionalRequestRunner
  def self.execute(array_of_arguments)
    request, params, on_success = array_of_arguments.shift

    request.execute(params) do |result|
      on_success.call result if on_success

      unless array_of_arguments.empty?
        execute array_of_arguments
      end
    end
  end
end
```

As you can see, `TransactionalRequestRunner#execute` is a recursive
method that runs a request, and within the request's success block, it
recursively calls itself to fire the subsequent request. It formalizes
the overall pattern and leverages all the features we've implemented.

## What we haven't discussed

We can make both essential and nice-to-have improvements to this code,
but I'll leave it as an exercise for the reader. For example:

- Make rollback asynchronous for fault tolerance and reducing the
  overall runtime;
- Improve HTTP response handling;
- Improve exception handling and isolate exceptions from third-party
  libraries;
- Support rolling back custom code that runs in between requests;
- Support traversing the linked list in the result object to collect
  data;
- Allow the `on_success` blocks in `TransactionalRequestRunner`
  to access the result of previous requests.

What other improvements do you have in mind?

## Wrapup

You can find the complete code examples here [in this gist].

I hope this article inspires you to up your external API code game! A
more advanced version of this pattern has been successfully used in
production in a system that moves millions of dollars. Regardless of
your app's size, these are all good practices you can adopt to make
your code more resilient and fault-tolerant. It's not specific to the
Ruby language, either.

If you want to dive deeper into the theory behind transactional API
requests with an eye on error handling and fault tolerance, I highly
recommend you read [the previous article].

I also recommend the first article in the series about [the resumable
pattern], which discusses a different approach for dealing with
external API requests.

[the previous article]: https://thoughtbot.com/blog/handling-external-api-errors-a-transactional-approach
[the resumable pattern]: https://thoughtbot.com/blog/handling-errors-when-working-with-external-apis
[fold right]: https://en.wikipedia.org/wiki/Fold_(higher-order_function)
[Command pattern]: https://gameprogrammingpatterns.com/command.html#undo-and-redo
[in this gist]: https://gist.github.com/thiagoa/3d09fa38455273b4b885533bd3de1390

## Learn about working with thoughtbot

Partner with us to learn how thoughtbot develops best-in-class Ruby on Rails applications. [Let's talk](https://thoughtbot.com/services/ruby-on-rails-development)!
