Communication between Rails engines

So you had the idea to separate different areas, A.K.A. domains, of your application using Rails Engines? You thought it was a great idea until have to communicate through two or more engines?

An example of what you want to do

This article explains how to do that communication, and how you can keep the productivity using Rails and a little bit of DDD.

If you are not familiar with Rails engines, they are great tools to separate concerns, reuse code, or simply namespace things. The most famous example of Rails Engine is Devise. You can learn much more reading the Rails guides about it.

A bank system

To illustrate the problem, I will follow the common case of a bank system. The engine one is called "Accounting", the engine two is called "Credit Report". They are two different domains of our Bank system. You can call them "subsystems".

The domain of this bank system

The Accounting engine is responsible for the transfer transaction that needs to trigger a new credit report for the accounts related to the transfer.

So we need to send a message to the "Credit Report" subsystem somewhere around the return of the "transfer" method.

Something like:

Don't do this

In this example you can see the message being sent to other domain ("CreditReport"), directly inside the "transfer money" use case.

This article could finish here, but this would lead to some problems: I'm invading a domain with another that does not have a strong relationship. It means that two subdomains are strongly connected. This is … not good.

Using dependency injection

An alternative to that is to inject the "CreditReport" subsystem into the "Accounting".

This can be done using a configuration, by setting the constant representing the Credit Report subsystem as string.

Line 8 does the magic of transforming strings into constants.

And this configuration would be done inside some initializer in the main app (Bank). The reason we are configuring using a string (and getting the constant dynamically) is that Rails can use auto load without problems.

We can change the "transfer money" use case to use that injected/configured dependency.

Using injected version

Good! Now the system is more flexible. There is still existing a contract between the two subsystems, but now "Accounting" does not need to know that a real "CreditReport" exist.

The problem with this approach is that if we want to test this engine in isolation, we will eventually need to depend on "Credit Report" engine or create a mock to represent that subsystem.

Using events

To avoid a strong relationship between the Accounting and Credit Report engines, we can create a Pub/Sub communication between them.

Watch out:

Low coupled version

You can see that the Credit Report dependency is gone. A new line get in. That line publishes the event with the result value.

This approach enables us to configure a subscriber class that receives the event and sends the message to the target, which is the Credit Report subsystem.

And the actual message to the "Credit report" goes inside the "AccountingTransferDoneEvent" class.

This class could go into "app/events/" in the main app.

A possible problem with this is that the events are almost async. You can't check if they work as expected in the publisher class. This is not a problem if you are dealing with something that can be treated as async job, and therefore can handle errors in isolation.

Conclusion

Communication between two domains is very common, and using Rails engines is easy to get separation of concerns while we keep the productivity that Rails brings to us.

Using dependency injection through configuration facilitates the communication, and enable us to easily change the implementation of the dependency. But it's much more flexible to have the event system when you can deal with async messages.

A POC using the event system can be found at https://github.com/philss/bank.


This article is highly inspired in the Clean Architecture ideas applied to Rails. Thanks to Fabiano B. for the insights and great discussions we always have. And thanks to the BankFacil folks, that started a great discussion about architecture using Ruby.