Encapsulating Business Transactions With Transflow

It’s a known fact that when you deal with a big problem it’s good to split it into smaller problems, solve them in isolation using separate components and use an integration layer to combine them into a single unit. Unfortunately it’s easier said than done. In an OO language like Ruby there are countless approaches you can take to tackle complex scenarios in your application.

Objects accumulating state, which gets mutated as a result of some business transaction, is already a complex thing to deal with. Turn that into a series of operations where each can mutate something and you can no longer reason about anything.

After spending some time working on functional data transformations and ROM, and diving into some functional languages a bit, I realized I want to tackle complex business transactions in the same way - by using simple function-like objects that respond to #call, receive input and return output, without causing any side-effects.

And so I sat down yesterday and wrote Transflow. Its first beta version is already on Rubygems.

Business Transactions?

A client sends a request to your application, your application sends a response; in the process of producing that response, many things need to happen - that’s a business transaction, a series of operations, each requiring some input to produce some output, where the last one returns the final response.

I really like showing simple concepts using procs, so here we go:

parse_input = -> input { JSON.parse(input) }
validate_input = -> input { input.key?(:id) ? input : raise("oops :id missing") }
find_user = -> input { user_repo.find(input.fetch[:id]) }

# a client asks for a user so:

input = '{"id": 1}'

find_user.call(
  validate_input.call(
    parse_input.call(input)
  )
)

I hear you screaming “that’s so not OO!”. I know, right. Bear with me.

The first thing to realize is that we capture the essence of what needs to happen. Do we need any state? Nope. So we use simple procs, it works well enough.

The second thing that we’re dealing with is transforming a string with a json request into another json string with the response. It requires some intermediate processing, validation and persistence, all of which can be nicely encapsulated using separate, re-usable components.

However, defining procs (or other callable objects) and calling them manually every time you use them would be too tedious, too much boilerplate. That’s why Transflow was created.

Defining a Transaction With Transflow

The gem provides an interface to define a business transaction flow. You must provide a container object which resolves objects that will handle individual steps. The rest is a simple DSL sugar on top of callable object composition.

Let’s define a transaction for our use-case:

parse_input = -> input { JSON.parse(input) }
validate_input = -> input { input.key?(:id) ? input : raise("oops :id missing") }
find_user = -> input { user_repo.find(input.fetch[:id]) }

container = {
  parse_input: parse_input, validate_input: validate_input, find_user: find_user
}

transflow = Transflow(container: container) do
  steps :parse_input, :validate_input, :find_user
end

input = '{"id": 1}'

transflow.call(input) # returns the user

This sounds almost too simple. So let’s say we actually want to update the user using the parsed input and then return it to the client.

Remember that each operation returns a result that is passed to the second one, this means you can simply do this:

parse_input = -> input { JSON.parse(input) }
validate_input = -> input { input.key?(:id) ? input : raise("oops :id missing") }

find_user = -> input { { input: input, user: user_repo.find(input.fetch[:id]) } }
update_user = -> input:, user: { user_repo.update(input, user) }

container = {
  parse_input: parse_input,
  validate_input: validate_input,
  find_user: find_user,
  update_user: update_user
}

transflow = Transflow(container: container) do
  steps :parse_input, :validate_input, :find_user, :update_user
end

input = '{"id": 1}'

transflow.call(input) # returns the updated user

Subscribing to Events

In many cases an external handler needs to be invoked when something happens. For example, in most systems, an email must be sent when a new user is created, or some background job must be scheduled when something is updated. Or maybe we need to write to a special log file when something fails.

With Transflow, you can subscribe event listeners to individual steps thanks to the awesome wisper gem.

It’s as simple as:

transflow = Transflow(container: container) do
  step :parse_input do
    step :validate_input do
      step :find_user do
        # this step will publish a `:update_user_success` or `:update_user_failure` event
        step :update_user, publish: true
      end
    end
  end
end

class UserEventListener
  def self.update_user_success(user)
    # do something
  end

  def self.update_user_failure(user, error)
    # do something
  end
end

input = '{"id": 1}'

transflow.subscribe(update_user: UserEventListener)

transflow.call(input) # returns the updated user and triggers UserEventListener

Container? Procs? What the heck

I’m using procs in the examples but please remember you can use anything that responds to #call(). If you’re wondering what’s the deal with the container - it’s a simple concept that makes Transflow unaware of the type of objects you’re using in your application. It focuses purely on composition, error handling and optional event publishing. This allows you to compose complex business transactions from small pieces and Transflow does not make any assumptions about your objects, it only relies on #call() interface.

Try it Out

If you’ve got complex scenarios in your application, try to decompose them into small, well encapsulated, callable objects and compose them into a transaction with Transflow. Even when your code doesn’t match the interface Transflow requires, it’s very easy to wrap your code with procs, give them meaningful names and compose a transaction using them.

There are more usage examples in the README.

Let me know what you think. I already ported a couple of crazy complex controller actions and it feels really good :)