Hexagonal Architecture + Rails
Last night, Fito and I watched Alistair Cockburn’s Hexagonal Architecture talk at the Excellence in Technology conference. We really enjoyed it and even though I thought I understood the pattern, I learned a lot. In fact, you should check it out before you read on. go ahead! I will wait…
Amazing!
So, when I first started learning about port and adapter patterns, I found it confusing. Eventually, I thought I understood it, but I overcomplicated it. And, for some reason, I never realized that the term “adapter” was a direct reference to adapter. Quad adapter mode. So it was a really exciting moment for me to see patterns represented so simply in code. And, it was the key to unlocking my understanding of patterns.
For those who skipped the video, why do it? Go and see it!
Port and Adapter Mode
Okay, now that you’ve all seen the video, here’s a quick review of port and adapter modes:
Your application code (your business or domain logic, whatever you want to call it) sits inside the hexagon and provides a “port” to which the outside world can connect. In statically typed languages, these ports are interfaces. In Ruby, they are simply the public API of a class in your application code (inbound), or a method that calls a service the application needs (outbound).
On the left side of the figure is the “inbound” or “driver” port. This is how the outside world interacts with your application. It’s your application programming interface (API). Your application code defines this interface.
On the right side of the picture is the “outbound” or “driver” port. This is how your application interacts with services available outside your application. This is your Service Programming Interface (SPI). Furthermore, it is your application code that defines this interface, preferably using the domain’s common language.
This is the “ports” part of the mode. Adapters are classes that sit between the outside world and APIs and SPIs. For example, you might choose to use a web framework as a way to adapt HTTP requests to your API. Alternatively, you can choose to use repository pattern Provides domain-centric SPI between you and your data storage.
So, for example, don’t call Task.create
On an ActiveRecord model named Task
you can call persistTask
method on TaskRepository
It is then converted into a category of ORM syntax. This leaves your application ignorant of the technology used for persistent data.
Why do this? Well, it allows you to swap different technologies for testing. Or, for long-lived applications, it allows you to replace older technology with newer, more advanced technology over time without causing you to change your business logic every time.
Here’s an example: your logging mechanism might write files to disk. However, perhaps with the emergence of tools such as Kafka, or the requirement to operate in Kubernetes, it may make sense to change this mechanism. If your application is tightly coupled to the logging mechanism, your application code must be modified to incorporate the new technology.
Or maybe you’re just switching your email provider from Sendgrid to Mailchimp. If your application code calls Sendgrid directly, it will need to be edited. However, if you introduce an adapter between your application code and the Sendgrid API, all you need to do is write a new adapter for Mailchimp.
Ports and Adapters + Rails
Rails is very opinionated. And, at first glance, it seems incompatible with the hexagonal architecture. However, if you look closely, you’ll see that Rails controllers can clearly be thought of as HTTP adapters for your application’s API. They receive an HTTP request, call your application, and then format the result into an HTTP response. That’s an adapter. (Although it does require you to abandon the traditional “call model from controller” approach to writing Rails applications.)
ActiveRecord is a little different. you Can Think of it as an adapter for your database. However, this is not the case. The adapter does not control the interface on either side. It simply implements the interfaces provided by applications and services through its SPI and API respectively. However, ActiveRecord does not implement application SPI. Instead, it provides its own interface in the form create
, find
, update
, delete
ETC.
Therefore, if your application makes ActiveRecord calls directly, you have coupled yourself to that interface. Now, if ActiveRecord’s syntax changes (it does), or if you decide to use a new technology that Rails doesn’t support, you’ll have to modify your application code. And, as you know, if you’ve read Fito’s expansion requires no modifications In this article, we prefer not to modify existing code if possible.
If I redraw the diagram above with Rails in mind, it might look like this:
Note that no adapter is required for testing. They can drive your API directly. Also note that different services may be configured in different environments. These may be simulated environments or even vendor-provided sandbox environments.
final thoughts
OK Let’s wrap this up…
Ports and adapters are a simple pattern that decouples business logic from I/O. It requires the “application” (business/domain logic) to provide an inbound (“driver”) API and an outbound (“driver”) SPI. Plus, it’s great for what we call inside-out development, where you first TDD the use cases in your application and then choose (or build) a framework to help your application interact with the outside world.
It’s entirely possible that Rails could serve as that framework. But not if you’re committed to The Rails Way™. For example, use a helper like form_for
Combine your views with models and completely bypass your application logic. Doing this means you bind the view to ActiveRecord. This is the same as calling Task.create
From within your application code. All data requests must go through the data storage adapter (repository) to maintain decoupling.
For many Rails developers, this is a step too far. They believe you don’t need to separate business logic from I/O. And, as the application grows and needs change over time, they will gradually stop viewing the Rails project as a fast, shiny Rails application and start to view it as legacy code. They complain about the legacy code they have to deal with. And, they’ll totally forgive Rails for getting them there.
After writing traditional Rails applications and systems that use the port and adapter pattern, I can tell you firsthand that water in a hexagon is better!
2024-12-07 04:48:30