Talks
ActiveJob: A Service Oriented Architecture

ActiveJob: A Service Oriented Architecture

by Andy Croll

In the video titled "ActiveJob: A Service Oriented Architecture" presented by Andy Croll at RailsConf 2015, the discussion revolves around the integration of ActiveJob in Rails applications to manage tasks outside of the request-response cycle effectively. Croll introduces the concept of ActiveJob, which provides a cohesive interface for various asynchronous job processing frameworks such as Delayed Job and Sidekiq, allowing developers to offload time-consuming processes like email sending and computations. Here are the key points discussed:

  • Introduction to ActiveJob:

    • Croll explains that ActiveJob, introduced in Rails 4.2, facilitates the execution of background jobs, helping to decouple long-running processes from the main application workflow.
  • Core Features of ActiveJob:

    • The interface includes methods like perform_now and perform_later to execute jobs immediately or schedule them for later.
    • ActiveJob supports ActionMailer, enabling background email processing, which is crucial when relying on external email services.
    • Introduction of Global ID for automatic lookup based on IDs within jobs is highlighted as a time-saving feature.
  • Service Objects Concept:

    • Croll draws a parallel between ActiveJob and service objects, which serve as a way to encapsulate business logic instead of using ActiveRecord models.
    • He emphasizes that service objects can enhance code clarity and organization, particularly in complex applications involving multiple models or external services.
  • Best Practices for Service Objects:

    • Recommended when handling complex operations and when multiple models or interactions with external APIs are involved.
    • Croll points out the importance of maintaining simplicity and avoiding over-engineering; he warns against the risks of dogmatic implementation of patterns.
  • Integration of Service Objects with ActiveJob:

    • Croll suggests that the patterns used in service objects can also be applied to ActiveJob, proposing that developers consider structuring their service objects as jobs to enhance their functionality and maintainability.
    • He notes that while this can introduce complexities, it can also align with Rails' design philosophy of addressing the common use cases.
  • Final Thoughts:

    • Croll cautions against pre-optimizing code and recommends focusing on clarity and simplicity. The integration of ActiveJob and service objects can help manage complexity effectively without overwhelming the application architecture.

In conclusion, Croll's talk highlights the effective use of ActiveJob for asynchronous job processing while advocating for the appropriate use of service objects to maintain a clean codebase in Rails applications. This approach can enhance overall application performance and developer experience.

00:00:12.240 Right, so I'm going to talk about ActiveJob. First of all, some introductions. Hello, I'm Andy. I work at House STP in the UK. It's a family-focused event. I live in Brighton on the south coast, and I work remotely. I run the Brighton Ruby Conference, which you should come to; it's lovely. Grim is speaking, and Sarah Alan is speaking in July for those of you who fancy a trip to the English Riviera. These are my twins; they are the reason I probably shouldn't have run the conference last year because they are two years old and absolutely exhausting.
00:00:29.559 Right, ActiveJob. Who knows what ActiveJob is? Show of hands please. Not that many. Who has actually used it in an app? Less. Okay, so from the recent release notes of Rails 4.2, it's our most recent Rails code. New code is good code, as we all know. Okay, this is another way of asking the same question. Who here has a Rails 4 app in production? Who has a Rails 3 app in production? Who has a Rails 2 app in production? My people! Who's got a Rails 1 app in production? Not you? Okay.
00:01:45.719 So for those of us in our day jobs who are perhaps a little behind the curve of the Rails edge, this is something to look forward to in the future. ActiveJob provides a stable interface to a bunch of queuing systems, allowing you to get work out of the request-response cycle. For example, sending email or doing something computationally expensive. It's essentially a frontend to Delayed Job, Resque, Sidekiq, or any of these other excellent queuing solutions.
00:02:01.399 It looks like this: pretty straightforward code. If you've written any asynchronous jobs, they all somewhat take this form. You do a bunch of stuff. Here’s the interface that you would call it with: you get a perform now and a perform later method freely available. When you’re using perform later, you can give it a time to delay before actually executing the job. Rails is nice, and the core team loves us very much. There’s also extra stuff. It powers ActionMailer. Every ActionMailer instance can be delayed, which is a good thing, particularly since many of us are using external services to send email. It makes sense to take that out of the request-response cycle in case that service is down or slow.
00:02:59.599 You also get Global ID. If you’ve written jobs before, you might have done this sort of thing where you passed the ID of the thing you're trying to do work to, and then inside the perform job, you would make a standard record lookup and then do the slow stuff. What Global ID lets you do is pass it in, and it automatically looks up based on that ID. I’m not going into the details of how it does that, but you can use all that time you saved to watch the Force Awakens trailer. So that's awesome, isn’t it? In summary, ActiveJob is a very good thing. It means you can change out your queuing system, which I know is something we all do every five minutes. But it’s a good idea; it’s pretty.
00:04:05.799 Alright, complete change of tack. As a community, we've been having interesting and heated discussions about the architecture inside our Rails apps, including the rise in understanding of service objects. To me, a service object is simply an object that executes a particular piece of business logic that you want to keep out of ActiveRecord. You’ll hear talk of hexagonal architectures, decorators, presenters, form objects, and components. A lot of the problems I’ve seen in legacy apps, including the one that I work on, relate to more fundamental design decisions, like bad data models.
00:05:02.160 I'm not pointing fingers, because we’ve all done it; I’ve done it, you’ve done it. As software agents, we get better understanding of our domain, so we support our mistakes. I’m not someone who’s going to service object everything; you do see this great framework on top of Rails, and I’m okay with there being a lot of frameworks already. However, I’m also not against putting stuff in non-ActiveRecord models. I’m anti-dogma, I’m friendly; I love everyone.
00:05:40.480 Some of you may be familiar with the terminology of Plain Old Ruby Objects (POROs). They are simply Ruby objects, but you put a name on them to give them a special designation. Service objects are a particular kind of way of using a Plain Old Ruby Object. They can also be quite useful for wrapping concepts that perhaps aren’t database-backed. Some people use things like form objects or wrap external libraries, which is something I quite like to do. You might abstract away your particular choice of HTTP wrapper, such as HTTParty.
00:06:24.800 So if you're going to use a service object, when might you try using one? If it's a complex operation, if you're going across multiple ActiveRecord models, or if you’re interacting with an external service, those are good scenarios for service objects. There's a link on the bottom of this slide to a really great article from a couple of years ago that covers ways to decompose your Rails app in a sensible way. The best thing about the article is that it indicates when this is a good idea to do, without being anti-dogmatic.
00:07:09.000 Typically, in apps where service objects are in effect, you end up with well-named verbs like 'do' or 'accept an invite,' for example. The examples I’m about to show you typically show one public method per class. You pass all the necessary inputs to the object, call a single method, and it executes the requisite tasks. Here’s an example of a style I’ve used for accepting an invite, presuming that accepting an invite is slow in your system for whatever reason.
00:08:12.000 When you call 'accept' on the invite model, you might do some logging because the business wants to know what's going on in your app, and it sends a couple of emails. This separation is a good practice. I’ve seen styles where you use a struct, so you don’t have to write the initialize method. As you can see, it’s the same call to the run method but adheres to the same concept.
00:08:33.600 Here’s another approach I’ve seen where they name the object with 'Service' at the end. You can also see dependency injections up there. For me, that can get overly complicated. Here’s another version using a struct, which allows you to invoke the service object without necessarily naming it. It’s not one back, but of course, you can do this. So we’ve seen different conventions that people use, and it's typical to agree on these with your team.
00:09:25.800 You've got methods called run, process, or call, etc. Sometimes people might use it to accept an invite, as you’d expect. Now, why am I telling you all this? My point is: why not use the pattern that we’re given by this new ActiveJob to organize all service objects? I guess my other point is whether this is just another way that DHH is trolling us when he says he doesn't like service subjects because this is his take on them.
00:10:09.840 Quick recap on 'Why Services?’ The act of dealing with complexity grows as you reach across multiple models or an external service. Those same concepts apply to jobs. The conventions I see for service objects can also be true for jobs. That’s where we need to position ActiveJob objects—in app jobs. They typically do stuff; there’s usually one method per class. They do one business thing and allow you to think about them asynchronously or synchronously.
00:11:08.000 This means that turning all your service objects into jobs is probably going to have some edge cases. However, in a couple of apps, such as toy applications that I’m working on, it seems to work quite well. This is what Rails is great for—solving the non-edge cases: the 80% that you do all the time seems like a good pattern to reuse. It’s interesting that ActiveJob might have solved something that many of us were already doing without realizing it.
00:11:58.400 This has been a relatively quick talk, so you can go try something else. After showing you this job of service objects, I’ve got a few things I’m going to carry on with. In all the examples I showed you, this is probably the pattern happening. In your controller, you will end up calling 'accept invite' using perform now or perform later—it doesn’t really matter. You pass in the invite current, and then the work actually gets done by a worker off to the side somewhere outside of the controller.
00:12:27.280 I probably wouldn’t put this example in the controller since we want it to be really slim. We’re not inclined to fabric controllers either, and now we have service objects where everything goes. Honestly, at some point, it’s hard to avoid a controller action becoming a list of tasks to perform, so it’s nice to have it all in one place. However, some of the issues we’ve seen with our legacy apps are the patterns, like service objects, being applied everywhere because it became the new standard.
00:13:09.320 This creates situations where the actual implementation of tasks is well away from where they’re called. Developers might find themselves jumping two or three levels of indirection, which can be complicated. It's essential to know where the code is executing tasks for better clarity and understanding in the codebase. There’s talk going on right now downstairs about controllers; I presume this is where our discussions are heading too. It's a clear message that being concise is important.
00:13:59.280 This method isn't going to work if you want to turn everything into a service object or apply your own custom Rails framework. In my experience, a recipe for bad code comes from trying to impose structure too early. It's crucial to use defaults and to resist the temptation to pre-optimize. It's often best to wait for tangible issues before applying complex patterns. Rails is excellent for extracting information from the database and generating useful pages.
00:14:32.480 Often, we forget that it's built for solving common use cases. If you approach it with strong patterns too soon, you might lose sight of its purpose. Consider viewing a service object like a job, something that may get pulled from the request-response cycle. It's worth contemplating whether this route brings any clarity to your controllers and the architecture of your application.
00:15:29.200 To conclude, let me reiterate: do not pre-optimize. This is the biggest weakness I see in my past work; revisiting old code often reveals complexities that weren't necessary at the time. Many of us have strong analytical skills, but let’s avoid overly complex solutions when simpler approaches exist. The best practices come from working well with Rails and its defaults. ActiveJob is fantastic for that.
00:16:10.000 My integration of service objects and ActiveJob can shine a light on how to use Rails effectively. Remember, it’s about getting things done efficiently and clearly. Each time you write code, keep the future in mind, including your future self, so you don’t create undue frustration for yourself. Thank you.
00:17:21.959 All.