Fat Models, Skinny Jobs
There’s an interesting piece of documentation Rails has about asynchronous processing using their Active Job component. From Rails Guides:
Looks harmless and helpful. But that “do something later” leaves a lot of developers in a dangerous place…
When Rails came on the scene in 2004–2005, I was an eager early adopter. But of course, we didn’t have many best practices yet which caused countless moments of cringe-worthiness looking back on code from (what feels like) eons ago.
The biggest messes were often how we organized our code and logic. Sure, we had our Model-View-Controller separation, but even those of us who were familiar using MVCs from other languages and frameworks still fell victim of littering code and logic everywhere.
In 2006 Jamis Buck introduced his philosophy of “Skinny Controller, Fat Model,” which was his way of organizing code that probably looks familiar to most of us using the framework today. In simple terms: he took most of his business and query logic and put it in his Models. Leaving HTML with what HTML is good at, and Controllers to specifically shuffle data from Models to Views.
Today, we have other best practices like Presenters and Concerns to help alleviate some bloat in Models. But still, Fat Models has been a rule of thumb that’s stood the test of time: get most of your business logic and queries in there, and you’ll enjoy the organization when it matters later.
But then I’ve seen a pattern in development teams using asynchronous Jobs.
Folks would start with a bunch of logic in their Models, but eventually realize how slow that logic was (iteration over large sets, external API calls, etc.). So they’d take that slow code, and cut & paste it into a Job class.
“Do something later” > becomes that slow pasted in code.
Now you have business logic sprinkled all over these Jobs. Bringing back the problems Jamis was helping us solve in 2006.
To avoid this, I keep these Jobs as skinny as possible. Instead of a block of “Do something later,” I’ll call a method on a Model that does that thing.
It’s not a difficult concept to grasp, but it’s also not something we talk a lot about. The actual README for Active Job in Rails hints at this pattern:
And it’s one I’m happy to adopt. But there’s more to it…
It’s Dangerous to Go Alone! Take This
Ruby developers have adopted another useful pattern where they use Bang method declarations for things that are “dangerous.” For example methods that change the object they are called on vs. making a copy.
name.downcase! vs. lowername = name.downcase
Well, running code synchronously that should be run asynchronously is pretty far out there on the dangerous spectrum.
So for any method that’s slow and needs async processing, I use a Bang method.
And then in a non-Bang version of the method I invoke the async version of the job.
And then let the Job use the Bang verison.
It’s a super useful habit then to stay away from the Bang version in your normal web-development. Use the Bang version if you know what you’re doing on a command line.
Nothing over Boilerplate
I hate boilerplate. It’s a huge reason I left the world of Java and EJBs behind for Ruby and Rails. Now, Rails still has its share of boilerplate, but these Active Job classes start to drive me nuts. Every time I want to run something asynchronously, I need to create a class and perform method that’s 5–6 lines of code?
So what I like to do is define a single Active Job like:
And use it for every instance I need a job. All that GoodJob class does us invoke whatever method it’s given asynchronously. So I have things like this in my code:
Without needing explicit Job classes every time.
Now, there used to be a helpful method in the days before Active Job that async libraries like DelayedJob and Sidekiq add to your models, in which you can skip my whole extra GoodJob class. A simple “delay” method:
Someone mentioned adding it to Active Job a couple years ago and it was shot down with “if you don’t use Job classes you won’t know which Jobs are running”. Eh. Fair point. But one I have failed to bump into in practice. If it’s that tough, keep a jobs.txt file around to help keep track vs. loading more code into memory. Cristian was kind enough to give us the gem.
Those are a few of the patterns I use with async processing in Rails. And if you are still looking for a way to do parallel processing in Rails that isn’t so infrastructure/OS/Ruby version dependant, stay tuned for Part 2! I use Active Job to do “synchronous” parallel processing :) I know. Sounds crazy, right?
P.S. If you need any help building software, give us a shout. I’m sure we can help.
And you should follow me on YouTube: youtube.com/nathankontny where I share more about how I run a business, do product design, market myself, and just get through life.
Originally published at www.rockstarcoders.com on August 29, 2018.