What service objects are
Service objects and Interactors are fashionable among Rails developers today. I can’t speak for everyone’s motives but the impression I get is that service objects are seen as a way to avoid the “fat models” you’re left with if you follow the “skinny controllers, fat models” guideline. If your models are too fat you can move some of their bulk to some service objects to thin them out.
Unfortunately it’s not quite possible to say what service objects are because the term “service object” means different things to different people. To some developers, “service object” is just a synonym for plain old Ruby object (PORO). To other developers, a service object is more or less an implementation of the command pattern. Interactors are similarly close to the command pattern.
For the purposes of this post we’ll go with the “command pattern” conception of a service object: a Ruby class with a single method, call
, which carries out a sequence of steps.
I have some gripes with the way service objects are advocated in the Ruby community. I’ll explain why.
Pattern vs. paradigm
The command pattern has its time and place. Sometimes it makes plenty of sense to bundle up an action into a single object and let that object orchestrate the steps that need to happen for that action.
But the command pattern is a pattern, not a paradigm. A software design pattern is a specific solution meant to meet a specific problem. A programming paradigm (like object-oriented programming or functional programming) is a style that can be applied at the level of entire applications.
What pains me a little bit to see is that many developers seem to have confused the idea of a pattern with the idea of a paradigm. They see service objects or Interactors and think, “This is how we’ll code now!” They have a service-object-shaped hammer and now everything is a nail.
Imperative vs. declarative
An imperative programming style expresses code mainly in terms of what to do. A declarative programming style expresses code mainly in terms of what things are.
For those not very familiar with imperative vs. declarative, here’s an explanation by way of example.
An imperative sandwich
If I were to tell a robot to make me a peanut butter and jelly sandwich using the imperative style, I would say, “Hey robot, get two pieces of bread, put some peanut butter on one, put some jelly on the other, put them together, put it on a plate, and give it to me.”
A declarative sandwich
If I were to do the same thing in a declarative style, I would say, “A sandwich is a food between two pieces of bread. It comes on a plate. A peanut butter and jelly sandwich is a sandwich that contains peanut butter and jelly. Make me one.”
My preference for declarative
I’m strongly of the view that declarative code is almost always easier to understand than imperative code. I think for the human brain it’s much easier to comprehend a static thing than a fleeting action.
At some point the things do have to take action (the peanut butter and jelly sandwich has to get made) but we can save the action part for the very highest level, the tip of the pyramid. Everything below can be declarative.
Service objects are all imperative
Service objects and Interactors are basically all imperative. Their names tend to be verbs, like NotifySlack
or AuthenticateUser
. Their guts tend to be imperative as well. To me this is a pity because, like I said above, I think most things are more clearly expressed in a declarative style. Even things that seem at first glance to lend themselves more naturally to an imperative style can often be expressed even more naturally in a declarative way.
An example of my style of coding
With debates around nuanced topics like this, small, contrived examples don’t really cut it. We need something with a little bit of complexity in order to see the differences that will be felt in production projects between service objects and my style of coding.
Before we get into the example, I’ll explain what my style of coding is.
Service objects vs. object-oriented programming
Instead of service objects or Interactors I just use regular old OOP. Not everything in Rails has to use a fancy tool or concept. Plain old objects are actually quite powerful and can get you a very long way.
Service objects might seem like OOP because they take the form of classes, but the service objects are objects only superficially. “Real” objects are abstractions of concepts that make it easier to deal with those concepts. If an object is just a wrapper for some procedural code, most of the benefits of objects are being left on the table.
What this example is
I work for a medical clinic. One of the things my application has to do is send a special file to an organization that helps facilitate the payment of insurance claims. This file is in a proprietary format and it’s called a ZirMed 837P File.
The job of bundling up a bunch of insurance claims, sticking them into a file one-by-one, and sending the file off to its destination might seem like a perfect use case for a service object. We could call it BuildZirMed837PFile
and run BuildZirMed837PFile.call
. But no, I choose to express this job in declarative terms. I hope that once you see the code you’ll agree that it’s better.
The tip of the pyramid
We’ll start with the highest-level code and work our way down. The tip of the pyramid is in fact imperative. It pretty much has to be. I’m creating an instance of a ZirMed837PFile::File
and calling the write
method on it, passing in the applicable appointments and the path of the destination file.
(Side note: all the following classes live inside the ZirMed837PFile
namespace. Because this is true, I’m able to call my root object just File
and it’s fine because it’s not going to be ambiguous or conflict with anything, thanks to the namespace.)
ZirMed837PFile::File.new(appointments).write(file.path)
The files we’ll be taking a look at are as follows.
app/models/zir_med_837_p_file/file.rb
app/models/zir_med_837_p_file/line.rb
app/models/zir_med_837_p_file/charge_collection.rb
app/models/zir_med_837_p_file/charge_slot.rb
app/models/zir_med_837_p_file/line_serializer.rb
Let’s take a look at the first class, File
.
The File class
This class is pretty small. We’re mainly interested in the write
method since that’s what’s being called above. The write
delegates most of its responsibility to the to_s
method.
# app/models/zir_med_837_p_file/file.rb
module ZirMed837PFile
class File
def initialize(appointments)
@appointments = appointments
end
def self.from_queued
new(Appointment.joins(:charges).merge(Charge.queued))
end
def write(filename)
::File.write(filename, to_s)
end
def filename
Rails.root.join("tmp", "charges.txt")
end
def to_s
@appointments.map { |appointment| Line.new(appointment).to_s }.join("\n")
end
end
end
The to_s
method in turn delegates most of its work to a class called Line
. Let’s take a look at that one.
The Line class
The Line
object is a bit more involved. Line
knows much more than File
regarding the peculiarities of the way the data needs to be formatted. But despite the complexity, there are just three relatively small methods: to_s
(declarative), fields_with_charges
(declarative), and an initializer.
# app/models/zir_med_837_p_file/line.rb
module ZirMed837PFile
class Line
TOTAL_NUMBER_OF_FIELDS = 213
FIRST_DIAGNOSIS_CODE_INDEX = 66
ATTRIBUTE_SLOTS = {
date_of_service: 77,
procedure_code: 101,
procedure_code_modifier: 107,
diagnosis_code_pointer: 113,
charge: 119,
units: 125,
ndc_code: 167,
}.freeze
def initialize(appointment)
@appointment = appointment
@charges = ChargeCollection.new(appointment.charges)
end
def fields_with_charges
LineSerializer.new(@appointment).fields.tap do |fields|
@charges.unique_diagnosis_codes.each_with_index do |diagnosis_code, offset|
fields[FIRST_DIAGNOSIS_CODE_INDEX + offset] = diagnosis_code
end
ATTRIBUTE_SLOTS.each do |attr, starting_index|
@charges.charge_slots.map do |charge_slot|
fields[starting_index + charge_slot.offset] = charge_slot.value[attr]
end
end
end
end
def to_s
TOTAL_NUMBER_OF_FIELDS.times.map do |index|
fields_with_charges[index + 1]
end.join("|")
end
end
end
We’re making use of a couple other even finer-grained classes here too, ChargeCollection
and LineSerializer
. Let’s see what these each do, starting with ChargeCollection
.
ChargeCollection
This is a tiny class. Just two methods, unique_diagnosis_codes
and charge_slots
, both declarative.
# app/models/zir_med_837_p_file/charge_collection.rb
module ZirMed837PFile
class ChargeCollection
def unique_diagnosis_codes
map(&:diagnosis_code).map(&:code).uniq
end
def charge_slots
ChargeSlot.charge_slots(self)
end
end
end
ChargeCollection
also makes use of another class, ChargeSlot
. Let’s see what this class does.
ChargeSlot
The “central” method of this class is value
, which basically just maps values to particular hash keys. Notice how yet again, all the methods in this class are declarative, not imperative.
# app/models/zir_med_837_p_file/charge_slot.rb
module ZirMed837PFile
class ChargeSlot
NUMBER_OF_CHARGE_SLOTS = 6
attr_reader :offset
def initialize(charges, offset)
@charges = charges
@offset = offset
@charge = charges[offset]
end
def value
{
date_of_service: I18n.l(@charge.date_of_service),
procedure_code: @charge.cpt_code.code,
procedure_code_modifier: @charge.modifier,
diagnosis_code_pointer: (diagnosis_code_index + 1).to_s,
charge: @charge.cpt_code.fee_per_unit.to_s,
units: @charge.units.to_s,
ndc_code: @charge.cpt_code.ndc_code,
}
end
def diagnosis_code_index
@charges.unique_diagnosis_codes.index(@charge.diagnosis_code.code)
end
def self.charge_slots(charges)
charge_slot_offsets.select { |offset| charges[offset].present? }
.map { |offset| new(charges, offset) }
end
def self.charge_slot_offsets
(0..(NUMBER_OF_CHARGE_SLOTS - 1))
end
end
end
This class doesn’t refer to any other classes so we’ve apparently reached a “leaf” of this dependency tree. Let’s now take a look at LineSerializer
which is used in the Line
class which we saw earlier.
LineSerializer
This is the final class in the group. The “central” method of this class is fields
which matches values with numbers slots in the file. (The strange proprietary format of this file is based on numbered slots.)
To cut down in what would have been a lot of noise inside of the fields
method, I’ve created a number of “helper” methods that get the data into the exact format they need to be in. Many slots and helpers have been omitted because they are numerous and wouldn’t help make the example any clearer.
I try to avoid class names that end in “-er” because that implies that the class doesn’t really represent an actual crisp concept. But sometimes “-er” is fine. (Heck, think about Rails’ ApplicationController
.) And plus, the benefits of a declarative style aren’t lost just because of the class name in this case. All the methods inside the class are of a declarative style.
# app/models/zir_med_837_p_file/line_serializer.rb
module ZirMed837PFile
class LineSerializer
PLACE_OF_SERVICE_CODES = {
"Office" => 11,
"Out Patient - Hospital" => 22,
"Out Patient - Surgery Center" => 24,
}.freeze
def initialize(appointment)
@appointment = appointment
end
def fields
{
1 => insurance_account.insurance_identifier,
2 => patient_name,
3 => patient_birth_date,
57 => rendering_provider_name,
89 => place_of_service_code,
184 => @appointment.physician.last_name,
185 => @appointment.physician.first_name,
188 => @appointment.location.name,
189 => address.line_1,
190 => address.line_2,
191 => "#{address.city}, #{address.state} #{address.zip_code}",
}
end
def patient_name
patient.name_on_insurance_card.presence || formatted_patient_name
end
def patient_birth_date
I18n.l(patient.birth_date)
end
def address
@appointment.location.address
end
def place_of_service_code
PLACE_OF_SERVICE_CODES[@appointment.location.type].to_s.tap do |code|
raise "Unable to find place of service code for location type \"#{@appointment.location.type}\"" if code.blank?
end
end
def rendering_provider_name
return "" unless insurance_account.insurance_type.medicare?
"#{@appointment.physician.first_name} #{@appointment.physician.last_name}"
end
end
end
End of example
That’s all. This job of packaging a set of appointments’ insurance claims into a proprietary file format, although it might seem like a decent candidate for a procedural/imperative chunk of code, is actually expressed (I think) quite nicely in a declarative OOP style.
Takeaways
- The command pattern is a pattern, not a paradigm. It’s not a style, like OOP or FP, that should be applied to all your code.
- Declarative code is often easier to understand than imperative code.
- Regular old OOP is a fine way to program. Not everything in Rails has to utilize a fancy tool or concept.
I can’t tell from this comment box if it will format the markdown correctly, so I uploaded my comment to a gist, as well: https://gist.github.com/JoshCheek/936c17369a9325e87a9caeb68e0052e1
—
Guessing that `ZirMed837PFile::File.new(appointments).write(file.path)` is supposed to be something like this:
“`ruby
file = ZirMed837PFile::File.from_queued
file.write(file.filename)
“`
Otherwise the `.from_queued` and `#filename` methods don’t make much sense. But
the `filename` method probably shouldn’t be an instance method, given that it’s
passed in to `#write` as an argument.
—
If we follow the appointments from `.from_queued` down into `to_s` and then into
`Line#initialize`, we see that each one has `.charges` called on it, which appears
to be an `ActiveRecord::Relation`. This will have an `N+1` query. Looking around
more, there are a bunch of these, there will be a lot of `N+1` queries (and probably
worse in some places)
“`ruby
def ZirMed837PFile::File.from_queued
new Appointment
.joins(:charges)
.includes(
:patient, # Just a guess, it wasn’t clear where this came from
:physician,
location: :address,
charges: [:diagnosis_code, :cpt_code],
)
.merge(Charge.queued)
end
“`
—
`Line#fields_with_charges` would be better private since its only called by `Line#to_s`.
—
`Line#fields_with_charges` appears to do a fairly expensive operation, and it
gets called 213 times by `#to_s`, so it would be good to memoize it. Then again,
it’s a hash, and the block just asks for keys, so you could also get away with
not memoizing it if you only call it once, which you can do with `Hash#to_proc`
(using a range to avoid needing to add 1 in the block):
“`ruby
def to_s
(1..TOTAL_NUMBER_OF_FIELDS).map(&fields_with_charges).join(“|”)
end
“`
—
`ChargeSlot.charge_slot_offsets` can be simpler if you use three dots, which will
exclude the final index
“`ruby
def self.charge_slot_offsets
0…NUMBER_OF_CHARGE_SLOTS
end
“`
—
`ChargeSlot.charge_slots` does a `select` that makes it look like some of the
charges could be empty, but it only does that b/c it’s iterating over a range that’s
potentially larger than the array. You can use `head` instead. It doesn’t fit the best
with the code as it currently is, b/c the code is more interested in the indexes than
the charges. But after refactoring those things out (passing it the charge instead of
the array and index), it will work nicely.
—
`ChargeCollection#charge_slots` winds up limiting the number of slots to 6 (because
of that above `charge_slots_offsets`), but `ChargeCollection#unique_diagnosis_codes`
does not. So if there were enough charges with different diagnosis codes, it would
write into other fields. It should probably be like
`unique_diagnosis_codes.take(NUMBER_OF_DIAGNOSIS_SLOTS)`
—
`I18n.l` is probably a hidden bug (meaning it could unexpectedly change the date format in prod)
because it renders localized dates. I’d check the spec, it probably specifies a format.
—
You don’t need to explicitly call `to_s` on the hash values, `join` will call it for you.
—
Note that the joining in `ZirMed837PFile::File#to_s` means there will not be
a newline on the last line. Seemed potentially incorrect.
—
With a hash, you can call fetch and it will invoke the block if the key DNE,
so you can refactor `place_of_service_code`
Also use `inspect` in the error message instead of typing quotes in it.
Because if there were quotes in `type.to_s`, then the displayed value would not
be correctly escaped. This will also prevent `nil` from looking like an empty string.
The values in `location.type` seem like an unwise choice, BTW. (1) it’ll be painful
to call an ActiveRecord column type (I assume it’s ActiveRecord), b/c AR will think
it’s single table inheritance. (2) The values look like they’re for presentation,
which makes them ill suited as a data representation, might convert it to an enum,
and look up the presentation string through `I18n`.
“`ruby
def place_of_service_code
PLACE_OF_SERVICE_CODES.fetch @appointment.location.type do |type|
raise KeyError, “Unable to find place of service code for location type #{type.inspect}”
end
end
“`
—
While reading it, I started tweaking it a bit. This is what I wound up at.
I didn’t really go into the `LineSerializer` because it was unclear where some
of that data was coming from. Note that I have no way of running any of this,
so who knows how correct it is.
“`ruby
# Opening the file allows it to write each line as it is generated so it doesn’t
# have to build up a potentially massive string in memory.
File.open Rails.root.join(‘tmp’, ‘charges.txt’), “w” do |file|
ZirMed837PFile::Document.new(Appointment.all).write file
end
require ‘csv’
module ZirMed837PFile
class Document
def initialize(appointments_relation)
@appointments = appointments_relation
.joins(:charges)
.includes( # `includes` fixes the N+1 queries
:patient, # Just a guess that this is here, it wasn’t clear where it came from
:physician,
location: :address,
charges: [:diagnosis_code, :cpt_code],
)
.merge(Charge.queued)
end
def to_s
write “” # Should work b/c `write` uses `<<`, which both IO and String implement
end
def write(stream)
# Receive the stream so you can write it into, eg, a socket,
# or some specific part of a currently open file.
each_line { |line| stream << line.to_s }
stream
end
def each_line
return to_enum __method__ unless block_given?
appointments.each { |appt| yield Line.new appt }
end
end
end
require 'csv'
module ZirMed837PFile
class Line
TOTAL_NUMBER_OF_FIELDS = 213
DIAGNOSIS_CODES_OFFSET = 65 # Starting it 1 earlier b/c document seems to count from 1 instead of 0
NUMBER_OF_DIAGNOSIS_SLOTS = 12 # I added this b/c first date of service is @ 77, which is 12 more than 65
NUMBER_OF_CHARGE_SLOTS = 6 # What happens if you have more than 6 charges? I'd expect that happens a lot
DATE_FORMAT = '%Y%m%d' # Not sure what it should be, but almost certainly not `I18n.l`
def initialize(appointment)
@appointment = appointment
end
def to_s
# Swapped the join("|") with CSV b/c it was just so sus to me, like you
# there's user submitted data in there, what if the user enters a pipe?
# that said, it's a total guess.
CSV.generate_line to_a
end
def to_a
(1..TOTAL_NUMBER_OF_FIELDS).map &to_h
end
def to_h
other_fields.merge diagnosis_code_fields, charges_fields
end
private
def other_fields
@other_fields ||= LineSerializer.new(@appointment).fields, :merge
end
# Maybe sort these to put the most expensive ones first? Someone might get denied their insurance claim b/c the aspirin was charge 1 and the MRI was charge 7 and thus omitted, making the bill look sus
def charges
@charges ||= appointment.charges.to_a
end
def diagnosis_code_pointers
@diagnosis_code_pointers ||= charges
.map(&:diagnosis_code).map(&:code).uniq
.take(NUMBER_OF_DIAGNOSIS_SLOTS)
.each.with_index(1).to_h
end
def diagnosis_code_fields
@diagnosis_code_fields ||= diagnosis_code_pointers.to_h do |code, offset|
[DIAGNOSIS_CODES_OFFSET+offset, code]
end
end
def charges_fields
@charges_fields ||= charges
.take(NUMBER_OF_CHARGE_SLOTS)
.flat_map.with_index { |charge, offset|
[ [offset+77, charge.date_of_service.strftime(DATE_FORMAT)], # date of service
[offset+101, charge.cpt_code.code], # procedure code
[offset+107, charge.modifier], # procedure code modifier
[offset+113, diagnosis_code_pointers[charge.diagnosis_code.code]], # diagnosis code pointer
[offset+119, charge.cpt_code.fee_per_unit], # charge
[offset+125, charge.units], # units
[offset+167, charge.cpt_code.ndc_code], # ndc code
]
}.to_h
end
end
end
“`
Great write-up!
Although FP is the new hotness, I still love when I see some very well-structured OOP code.
Thank you for sharing.
Thanks!
I like that you are explaining your style through a more nuanced, real world example that is still reasonably easy to follow.
The way you are adressing the topic is pretty much how I’ve made up my mind about this topic.
Another thing that really botheres me with the `.call` pattern in the case of interactors is the hassle with discoverability. If all your methods are just named `.call` it’s hard to search for locally, in github, in your exception tracker…
One thing that I’d like to comment on is your `ChargeCollection` class. I understand the desire to add functionality to collections, but I generally refute when someone extends from base-classes like Array, Hash etc. It’s usually more appropriate to put this logic elsewhere. I think the only exception to this is when you are building a framework where all objects are interacting with a same set of base-classes-on-steroids.
Thanks! And point taken regarding the collection. I think I no longer agree with my past decision on that one.
Great article!
I agree that sometimes service objects get a little abused.
However, in most of the cases they are used as part of the CRUD set of actions, where the majority of the logic revolves around building the object to be stored in the DB + maybe sending some notifications etc.
How would you model something like that with this OOP approach?
Also, in your example, who would call the initial #write method? The controller?
One final thing, I’m not sure that app/models is the right place to put this kind of code. app/models is usually reserved for Active Record models. I would expect your code to be under /lib instead.
Thanks! I’ll answer your questions in reverse order. Regarding where to put things, I put all my domain logic in app/models and all my application logic in lib. The #write method happens to get called just by another model in this case. Regarding how I would model something like that in OOP, I’m not sure. Maybe I wouldn’t. I’d have to understand more about the scenario in order to know how I would code it.
https://www.youtube.com/watch?v=MQw9zF9IehI
Jason – Thanks for sharing this article. I definitely agree with you that an OOP design often is a better fit and helps with creating more understandable/manageable code.
Have you considered combining OOP design with Service Objects/Interactors? Doesn’t really fit the more common definitions of Service Objects but that is what I’ve been doing for a while.
The basic structure still includes a `call` class-level method but what the `call` method does is where things tend to diverge. For me, the `call` method is just a simple way to create an instance of the Service Object, run it by calling an instance method (I use `go` but could be anything), and return the created instance.
There are many reasons for this Service Object design but it primarily came out of trying to figure out what a Service Object should return. I’ve always thought that suggestions for a Service Object return value were either too complex (creating a special return object) or too opaque (no return value, check the database to make sure it worked).
Returning the Service Object instance allows for an OOP design on top of the Service Object pattern. Then you can design the Service Object so that it’s easy to inspect what the Service Object did (ex: new objects created or updated). In the case of report generation, like in your example, you would have public methods for retrieving the part of the report that the Service Object is responsible for.
Perhaps keeping the Service Object pattern isn’t that important to you but it wouldn’t change too much of your code. You would keep most of the same classes, add a few standard methods to each object, and maybe do some minor restructuring. I also like to name my Service Objects as verbs (`LineSerializer` -> `SerializeLine`) to keep me in the right frame of mind when writing the code but I don’t see that as important as the basic structure.
A couple of other things I think about when designing this type of code is writing automated tests and background jobs. The OOP Service Object works well for those situations too.
Thanks for writing this up Jason. I must say I have contracted a bit of a service-object-itis in that lately almost everything I’ve written, that is not a model or a controller, has been a service.
My justification for that is that web applications lend themselves to the (over)use of command pattern well. Action code works well by making a command call and rendering the return value. Job code works the same way because return value is irrelevant. Ditto for callbacks. Perhaps the only place for objects with several methods in Rails’ MVC is the missing “view” object.
From the perspective of a service-object-junky
ZirMed837PFile::File.new(appointments).write(file.path)
looks hard to test (will have to assert/mock instantiation and call to write), and requires the developer to remember both how to instantiate and then what to call on the instance. The API can be KISSed down to one method call.
A service call enthusiast would say that for the purposes of writing the ZirMed837PFile, the calling object does not care, indeed ideally should not have access to, any writing or aggregating instances. Merely a writer service that, when called with appropriate params performs the needed operation, some
ZirMed837PFileWriter.call(
appointments: appointments,
path: file.path
)
This is simple to contract-test – assert that writer was called with correct params, mock returned value, if any, and assert that the value was used correctly.
Perhaps this is dumbing Ruby down to BASIC and losing some OOP magic, but I find that when working in a team the understanding that everyone writes services that have just one public method and only use kwargs to be liberating. Understanding another’s service generally only requires reading the “.call” spec.
Thanks for the article! It’s always nice to have a look on well structured code. But there is a one point in my mind about this approach vs .call . The deal is you approach is basically a freedom, it fits to experienced programmers who knows how to decompose their code and not mess it up and the .call approach pushes less experienced developers to more simple and narrow interface so their code looks similar so new developers can stick to it and save good structure instead of practising in decomposing task to plain objects which is not simple at all for less experienced developers. Anyway I think both approaches can be used in the same project with good balance.
Thanks! I would argue that if the .call approach results in a more simple and narrow interface, and if a more simple and narrow interface is easier to understand, then everyone should do it that way, not just less experienced developers. But I don’t think using .call does result in more understandable code. I think using .call gives the illusion of simplicity and organization without genuinely providing simplicity and organization. What I would prefer is that less experienced developers are taught good OOP practices.
Interesting and thought-provoking.
What do you think about “SerializedLine” instead of “LineSerializer”, to preserve the declarative naming convention and avoiding “er”.
Thanks. Maybe that would be better, not sure
Thank you, interesting opinion.But I think those classes shouldn’t be in models, it rather lib folder, since you could re-use it in other projects, or even extract it to gem. Models just for AR, plain objects there are confusing.
No, these aren’t things I would extract to a gem, since they’re specific to my domain model. In my conception of models, they’re not limited to just AR.
I think you’ve added unnecessary confusion to your argument here, and overlooked that service objects can also use a rich domain model to get things done.
The essence of your argument seems to say this:
ZirMed837PFile::File.new(appointments).write(file.path)
is better than this:
class MyInteractor
def execute
ZirMed837PFile::File.new(appointments).write(file.path)
end
end
I understand some may be inclined to dump all kinds of imperative logic in their service objects, but I have rarely seen that in the wild. What usually happens is the service objects instantiate and coordinate with other service or domain objects.. the result would have been exactly the same as what you propose in your article except the top level call wrapped in a service object.
?