DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Protecting Your Domain-Driven Design from Anemia
  • Cutting-Edge Object Detection for Autonomous Vehicles: Advanced Transformers and Multi-Sensor Fusion
  • Simplify Authorization in Ruby on Rails With the Power of Pundit Gem
  • GenAI: From Prompt to Production

Trending

  • Infrastructure as Code (IaC) Beyond the Basics
  • The Full-Stack Developer's Blind Spot: Why Data Cleansing Shouldn't Be an Afterthought
  • Integrating Model Context Protocol (MCP) With Microsoft Copilot Studio AI Agents
  • Metrics at a Glance for Production Clusters
  1. DZone
  2. Coding
  3. Frameworks
  4. Using Rails Service Objects

Using Rails Service Objects

In this article, we'll find out what Rails Service Objects are and how you can use them to make your app cleaner and maintain it.

By 
Aleksandr Ulanov user avatar
Aleksandr Ulanov
·
Sep. 29, 22 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
5.2K Views

Join the DZone community and get the full member experience.

Join For Free

If you're developing web apps using Ruby on Rails, you probably already know that Rails is an MVC (Model-View-Controller) framework, which means that you have your Models responsible for data, Views responsible for templates, and Controllers responsible for request handling. But the bigger your app gets, the more features it has - the more business logic you will have. And here comes the question, where do you put your business logic? Obviously, it's not viewed that should handle it. Controllers or Models? That will make them fat and unreadable pretty soon. That's where Service Objects come to the rescue. In this article, we'll find out what are Rails Service Objects and how you can use them to make your app cleaner and keep it maintainable.

Let's say you have a project for handling cab trips; we'll take a look at the particular controller action, which updates trip records. But it should not only update trips based on user input params (e.g., starting address, destination address, riders count, etc.), but it should also calculate some fields based on those params and save them to the database. So, we have a controller action like this:

Ruby
 
# app/controllers/trips_controller.rb
class TripsController < ApplicationController
  def update
    @trip = Trip.find(params[:id])

    if update_trip(trip_params)
      redirect_to @trip
    else
      render :edit
    end
  end

  private

  def update_trip(trip_params)
    distance_and_duration = calculate_trip_distance_and_duration(trip_params[:start_address],
                                                                 trip_params[:destination_address])
    @trip.update(trip_params.merge(distance_and_duration))
  end

  def calculate_trip_distance_and_duration(start_address, destination_address)
    distance = Google::Maps.distance(start_address, destination_address)
    duration = Google::Maps.duration(start_address, destination_address)
    { distance: distance, duration: duration }
  end
end


The problem is that you’ve added at least ten lines to your controller, but this code does not belong to the controller. Also, if you want to update trips in another controller, for example, by importing them from a CSV file, you will have to repeat yourself and rewrite this code. Or you create a service object, i.e., TripUpdateService, and use that in any place you need to update trips.

What Are Service Objects?

Basically, a service object is a Plain Old Ruby Object ("PORO"), a Ruby class that returns a predictable response and is designed to execute one single action. So it encapsulates a piece of business logic.

The job of a service object is to encapsulate functionality, execute one service, and provide a single point of failure. Using service objects also prevents developers from having to write the same code over and over again when it’s used in different parts of the application.

 All service objects should have three things:

  1. An initialization method;
  2. A single public method;
  3. Return a predictable response after execution.

Let's replace our controller logic by calling a service object for trip updates:

Ruby
 
class TripsController < ApplicationController
  def update
    @trip = Trip.find(params[:id])

    if TripUpdateService.new(@trip, trip_params).update_trip
      redirect_to @trip
    else
      render :edit
    end
  end
end


Looks much cleaner. Now let's take a look at how we implement a service object.

Implementing a Service Object

In a Rails app there are two folders that are commonly used for storing service objects: lib/services and app/services. Basically, you can choose whichever you want, but we'll use app/services for this article.

So we'll add a new Ruby class (our service object) in app/services/trip_update_service.rb:

Ruby
 
# app/services/trip_update_service.rb
class TripUpdateService
  def initialize(trip, params)
    @trip = trip
    @params = params
  end

  def update_trip
    distance_and_duration = calculate_trip_distance_and_duration(@params[:start_address],
                                                                 @params[:destination_address])
    @trip.update(@params.merge(distance_and_duration))
  end

  private

  def calculate_trip_distance_and_duration(start_address, destination_address)
    distance = Google::Maps.distance(start_address, destination_address)
    duration = Google::Maps.duration(start_address, destination_address)
    { distance: distance, duration: duration }
  end
end


Alright, service object added; now you can call TripUpdateService.new(trip, params).update_trip anywhere in your app, and it will work. Rails will load this object automatically because it autoloads everything under app/ folder.

This already looks pretty clean, but we can actually make it even better. We can make the service object execute itself when called so that we can make calls to it even shorter. If we want to reuse this behavior for other service objects, we can add a new class called BaseService` or ApplicationService and inherit from it for our TripUpdateService:

Ruby
 
# app/services/base_service.rb
class BaseService
  def self.call(*args, &block)
    new(*args, &block).call
  end
end


So this class method named call creates a new instance of the service object with arguments or blocks passed to it and then calls the call method on that instance. Then we need to make our service inherit from BaseService and implement the call method:

Ruby
 
# app/services/trip_update_service.rb
class TripUpdateService < BaseService
  def initialize(trip, params)
    @trip = trip
    @params = params
  end

  def call
    distance_and_duration = calculate_trip_distance_and_duration(@params[:start_address],
                                                                 @params[:destination_address])
    @trip.update(@params.merge(distance_and_duration))
  end

  private

  def calculate_trip_distance_and_duration(start_address, destination_address)
    distance = Google::Maps.distance(start_address, destination_address)
    duration = Google::Maps.duration(start_address, destination_address)
    { distance: distance, duration: duration }
  end
end


Then let's update our controller action to call the service object correctly:

Ruby
 
# app/controllers/trips_controller.rb
class TripsController < ApplicationController
  def update
    @trip = Trip.find(params[:id])
    if TripUpdateService.call(@trip, trip_params)
      redirect_to @trip
    else
      render :edit
    end
  end
end


Where Should You Put Your Service Objects

As we've discussed earlier, two base folders for storing service objects are lib/services and app/services, and you can use whichever you want.

Another good practice for storing your service objects will be storing them under different namespaces, i.e., you can have TripUpdateService, TripCreateService, TripDestroyService, SendTripService, and so on. But what will be common for all of them is that they're related to Trips. So we can put them under the app/services/trips folder, in other words, under the trips namespace:

Ruby
 
# app/services/trips/trip_update_service.rb
module Trips
  class TripUpdateService < BaseService
    ...
  end
end

  

Ruby
 
# app/services/trips/send_trip_service.rb
module Trips
  class SendTripService < BaseService
    ...
  end
end


Don't forget to use new namespace when calling those services, i.e. Trips::TripUpdateService.call(trip, params), Trips::SendTripService.call(trip, params).

Wrap Your Code in Transaction Block

If your service object is going to perform multiple updates for different objects, you better wrap it in a transaction block. In this case, Rails will roll back the transaction (i.e., all of the performed db changes) if any of the service object methods fail. This is a good practice because it will keep your db consistent in case of failure.

Ruby
 
# archive route with all of its trips
class RouteArchiver < BaseService
  ...
  def call
    ActiveRecord::Base.transaction do
      # first archive the route
      @route.archive!

      # then archive route trips
      trips = TripsArchiver.call(route: @route)

      # create a change log record
      CreatChangelogService.call(
        change: :archive,
        object: @route,
        associated: trips
      )

      # return response
      { success: true, message: "Route archived successfully" }
    end
  end
end


It's a simple example of updating multiple records in a single transaction. If any of the updates fails with an exception (e.g., route can't be archived, changelog create fails), the transaction will be rolled back, and the db will be in a consistent state.

Passing Data to Service Objects and Returning Response

Basically, you can pass to your service objects almost anything, depending on the operations they perform. ActiveRecord objects, hashes, arrays, strings, integers, etc. But you should always pass the minimum amount of data to your service objects. For example, if you want to update a trip, you should pass the trip object and the params hash, but you should not pass the whole params hash because it will contain a lot of unnecessary data. So you should pass only the data you need, i.e., TripUpdateService.call(trip, trip_params).

Service Objects can perform complex operations. They can be used to modify records in the database, send emails, perform calculations or call 3d party APIs. So it's quite possible that something can go wrong during those operations. That's why it's a good practice to return a response from your service objects. You can return a boolean value or a hash with a boolean value and some additional data. For example, if you want to update a trip, you can return a boolean value indicating whether the trip was updated successfully or not, and you can also return the trip object itself so that you can use it in your controller action.

The thing you should keep in mind, though, is that your response from the service object should be predictable. It should always return the same response, no matter what. So if you return a boolean value, it should always return a boolean value, and if you return a hash, it should always return a hash with the same keys. This will make your service objects more predictable and easier to test.

What Are the Benefits of Using Service Objects?

Service Objects are a great way to decouple your application logic from your controllers. You can use them to separate concerns and reuse them in different parts of your application. With this pattern, you get multiple benefits:

  • Clean controllers. The controller shouldn't handle business logic. It should be only responsible for handling requests and turning the request params, sessions, and cookies into arguments that are passed into the service object to perform the action. And then perform redirect or render according to the service response.
  • Easier testing. The separation of business logic to service objects also allows you to test your service objects and your controllers independently.
  • Reusable Service Objects. A service object can be called from app controllers, background jobs, other service objects, etc. Whenever you need to perform a similar action, you can call the service object, and it will do the work for you.
  • Separation of concerns. Rails controllers only see services and interact with the domain object using them. This decrease in coupling makes scalability easier, especially when you want to move from a monolith to a microservice. Your services can easily be extracted and moved to a new service with minimal modification.

Service Objects Best Practices

  • Name rails service objects in a way that makes it obvious what they're doing. The name of a service object must indicate what it does. With our trips example, we can name our service object like: TripUpdateService, TripUpdater, ModifyTrip, etc.
  • Service Object should have a single public method. Other methods must be private and be accessible only within a particular service object. You can call that single public method the way you want, just be consistent and use the same naming for all your service objects.
  • - Group service objects under common namespaces. If you have a lot of service objects, you can group them under common namespaces. For example, if you have a lot of service objects related to trips, you can group them under the Trips namespace, i.e., Trips::TripUpdateService, Trips::TripDestroyService, Trips::SendTripService, etc.
  • Use syntactic sugar for calling your service objects. Use proc syntax in your BaseService or ApplicationService and inherit from it in other services. then you can use just .call on your service object class name to perform an action, i.e., TripUpdateService.call(trip, params)
  • Don't forget to rescue exceptions. When a service object fails due to an exception, those exceptions should be rescued and handled properly. They should not propagate up to the call stack. And if an exception can't be handled correctly within the rescue block, you should raise a custom exception specific to that particular service object.
  • Single responsibility. Try keeping single responsibility for each of your service objects. If you have a service object that does too many things, you can split it into multiple service objects.

Conclusion

Service objects are a great way to decouple your application logic from your controllers. They can be used to separate concerns and reuse them in different parts of your application. This pattern can make your application more testable and easier to maintain as you add more and more features. It also makes your application more scalable and easier to move from a monolith to a microservice. By the way, Ruby on Rails is used for this example only; you can use the same pattern with other frameworks.  If you haven't used service objects before, you should try them.

Business logic Ruby (programming language) Object (computer science) Trip (search engine)

Published at DZone with permission of Aleksandr Ulanov. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Protecting Your Domain-Driven Design from Anemia
  • Cutting-Edge Object Detection for Autonomous Vehicles: Advanced Transformers and Multi-Sensor Fusion
  • Simplify Authorization in Ruby on Rails With the Power of Pundit Gem
  • GenAI: From Prompt to Production

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!