Improve up your Rails controllers using a pub/sub pattern

Piotr Kotnis
5 min readAug 12, 2019
Photo by Jason Rosewell on Unsplash

TL;DR — This article will introduce you to the pub/sub pattern, and show you how to improve your Rails controllers by implementing it using the whisper gem.

Ruby on Rails is an amazing and mature framework, which many of us instinctively turn into whenever we need to get something simple up and running as soon as possible. However, many will also agree that Rails simplicity is also its biggest flaw. As your application grows, your sleek-and-fast Rails app suddenly become messy and disorganized. I like the MVC pattern, but when the number of dependencies between different entities grows, you need something more than that.

Publisher & Subscriber

One approach I really wanted to try out for some time was the pub/sub pattern. In this pattern, some objects publish special messages (with an optional payload), and other subscribed objects are listening to them. Whenever a subscribed object hears a message it is interested in, it can do something with it. whisper gem adds pub/sub capabilities to Ruby objects, so why not try using that in our Rails app.

Let’s take a look at this hypothetical Rails controller which does the following:

  • When a Post is created, it sends an email notification to its author
  • If the post has 300 words or more, it creates a FeaturePost object too.

This simple implementation looks OK, but it violates one common principle — the Single Responsibility Principle (SRP). Why? Well, our PostsController is now responsible not only for creating posts, but also sending emails and creating other objects (FeaturedPost). And if you’re planning to write a test for that controller, you should probably test all those additional things too. PostsController sending emails? This ain’t good.

BTW, for the rest of the article, I’m going to skip the def post_params part of the controller, in order to save you time reading.

So let’s try to implement a service object that will create a Post , and nothing else — and then, depending on the result of that creation, it’s going to publish some sort of Success or Failure message.

Ok, so if post.save returns true, we broadcast post_created string, together with the Post object we just created — but if post.save returns false, we broadcast post_not_created string. Sounds easy so far. Let’s hook it up into our PostsController

This looks promising, but we’re missing something important here. Every Rails controller should have some sort of redirect_to or render , right? Let’s use the messages that Services::Post::Create broadcasts, and let’s add that back to our controller. We’re going to use the on method that comes from whisper.

Better, but there are still two pieces missing. Our original controller was also sending email notification and creating aFeaturedPost object, and this controller does not. And we don’t want to put that inside that controller, because this is exactly what we’re trying to avoid.

But what if we could tell someone to listen for that post_created message, and then, when that message is broadcasted, send an email notification, and create a FeaturedPost ?

pub/sub subscriptions to the rescue. Let’s create two listeners objects.

Ok, so now we have two objects. Listeners::Mailer will be responsible for sending an email notification to the author of that post, and Listeners::FeaturedPost will be responsible for creating a FeaturedPost but only when the post has more than 300 words.

Note how the methods defined inside those listeners are named exactly the same as the message string that Services::Post::Create broadcasts. This is very important because this connects a Listener with our Service

Ok, let’s add those Listeners to our controller.

So what we did here is we’ve subscribed both listeners (using a subscribe method) to ourServices::Post::Createservice object, so that any message broadcasted by Services::Post::Create will be passed down to those listeners. And if that message happens to be post_created , then both Listeners::Mailer#post_created and Listeners::FeaturedPost#post_created methods are going to be executed.

Voilà 🎉 Our app now does the same thing as it did at the beginning of this article.

I really like using this pattern because it’s really simple to start with and it really helps you spread responsibilities across many smaller objects.

If you look at the PostsController now, you will notice it’s responsible for only one thing. One could think it's a creation of aPost , but that’s not true. If you look closely, you will notice that PostsController does not create Postobjects per se — instead, it runs some service, nothing else. So in practice, the controller becomes a layer that converts incoming HTTP requests into a call method call on some service class. This drastically simplifies writing tests.

My Services::Post::Create is a small & compact object which does one thing only — it tries to persist w post in a database and broadcasts a proper message depending on the result. Same with bothListeners — each one is responsible for one small tiny piece of our business logic, and nothing else. This makes those object really easy to test too.

Want more? Here are a few recommendations

There are a few things you might want to consider when using this pattern.

  1. Use Ruby constants to store messages

Currently, the post_created message string must match the method name inside your listeners' classes. If you make a typo in one of the places, it might be hard to catch what’s wrong. That’s why I recommend using constants to store those string.

Then, make sure you publish them through your service class.

Next, make sure your controller uses them too.

And last but not least — use them to define methods inside your listeners

That way, if you make a typo in a constant name, your app will immediately throw a NameError: uninitialized constant error at you :)

2. Don’t be afraid to use other service objects inside your listeners

In our implementation, Listeners::Mailer is actually responsible for sending emails, and Listeners::FeaturedPost is responsible for creating FeaturedPosts . I think it’s OK, but I think it would be even more kosher to maybe have a separate service object to do just that, and use listeners as a layer that only turns incoming pub/sub messages into other services.

This is how this could look like

I really hope some of you will find this pattern interesting, and you will give it a try. I highly recommend it. If you have any questions, please let me know in the comments below.

--

--

Piotr Kotnis

Programming, games, music, photography, food & coffee.