Look at any Rails codebase that's older than a few months. I bet you'll find a sub-directory of app
called services
.
It comes with the best of intentions for separation of concerns. The developers want to keep controllers straightforward and focused on understanding a request and providing a response. They also want to keep models small, because those files have gotten too big and tedious to navigate and modify.
What are service objects anyway?
I think sometimes it's supporting code, "infrastructure" if you will. For example an HTTP client for an API. I am not going to say much about this type of code. Put that in a lib
directory if you must, and go on with your day.
Sometimes a "service object" performs a series of steps, often with some conditional logic. Send a welcome email only if the user record was successfully created.
Other times it's seemingly a single operation, but with a long implementation hidden away in private methods. You didn't want those polluting your ActiveRecord model class, so you put them here.
At least that's how I've used and seen these service objects used. I've been writing Rails applications since 2011 and I still don't understand why we call this concept a service object. Maybe I should read the Domain Driven Design book.
Aside: suffixes for the sake of suffixes
Rails has a convention of suffixes. It's a UsersController
, and a UserMailer
, and an ArchivalJob
. So developers try to follow that convention and name something UserSignupService
. Again, because I don't understand what a service is, I keep reading the meaningless suffix everywhere. Why not call it UserSignup
or Registration
?
A representation of a business domain concept is called a model
Let's come back to my main point. I think we should stop thinking in terms of services and start thinking in terms of domain objects, otherwise known as models.
I admit "model" is a generic name in itself. However it is a core concept in Rails' Model View Controller architecture and it is widely understood.
A technique I use is to step back from the code and think about the noun or noun groups used to describe concepts in the domain of the application. That also works for processes or operations.
For example, when talking about sending notifications, the noun is "notification". A noun group that comes to mind is "notification delivery". Rather than a NotificationService
or a NotificationManager
, I define a Notification
with a deliver
method.
class Notification
def initialize(title:, body:)
# ...
end
def deliver
# ...
end
end
Depending on the domain, you can be more specific with the nouns. In case the delivery is a complex concept, I would extract a dedicated NotificationDelivery
class. If there is something special about the formatting of the notification, that logic could go in a NotificationPayload
.
I avoid the names with the shape of SomethingDoer
or DoesSomething
.
Naming is hard, but taking the time to pick a good name is worth it. You'll be able to explain the concepts more easily. The application routes will fall into place. The controllers will make sense. Watch In Relentless Pursuit of REST for some inspiration.
Aside: directory structure
I've tried adding nuanced directory structures in projects. For example having categories such as errors
, forms
, policies
, presenters
, serializers
, validators
, etc. I think some amount of organisation is good, but I've definitely overdone it and I tend to put a lot more in the models
directory nowadays.
With fuzzy finders, go to definition, and other tooling, the location of a file in a project doesn't really matter. In fact, I don't use the project file tree view when programming.
app/services
doesn't bother me from a discovery perspective. However, I think that the models
directory encourages you to name the domain object in a better way. Another benefit is that you avoid having to choose where to save a new file.
If you do feel strongly about directory structure, I find namespaces to be a great way to keep related concepts together.
Namespaces help with organisation
Namespaces will not only lead you to better names, but also you'll start putting things that belong together close by in the file system. Here is a good example I've seen in production code.
The price of a line item depends on the product and product options. The code for calculating the price has become too large and complex, and the maintainers have extracted it from a method on the LineItem
model to its own class.
class LineItem < ApplicationRecord
def price
LineItems::Price.new(self).calculate
end
end
Now if you want to understand the pricing code, it's in one clearly delineated place. The implementation details are no longer mixed with other (private) methods of the LineItem
class.
The code is also close by in the project structure.
line_item.rb
line_items/
price.rb
What's powerful in this example is that you retain the interface of the model (line_item.price
) while separating self-contained parts of the implementation. That makes the abstractions easier to discover. You're not wondering whether you can call a method on the model directly or you have to hunt down some class in app/services
. It goes "along the grain" of Rails.
Continuing the example about notifications from earlier, the Notification
model could be the public interface, but different aspects about notifications could be in models in the Notifications
namespace. For example:
class Notification
def initialize(title:, body:)
@payload = Notifications::Payload.new(title: title, body: body)
end
def deliver
Notifications::Delivery.new(@payload.format).perform
end
end
To be clear, I would start with fewer objects, and only introduce the extra ones as the application complexity increases over time.
Try it out
The next time you're adding a new class, whether it's in your current codebase or a new project, consider the tips about naming and organisation above. See how it affects the code's design.