Once upon a time I was writing a web app that needed to accept notifications of payments. Once it was notified of a payment (via a webhook) it needed to take certain actions to fulfill the purchase.

An overweight controller

Imagine that you’re working on this app. The payment notifications come in the form of PayPal-style IPN data. Here’s an approximation of the controller action for receiving these notifications:

(Note that this is a Sinatra action, but an equivalent Rails controller action wouldn’t be much different.)

post "/ipn" do
  demand_basic_auth

  # Record the raw data before we do anything else
  email = params.fetch("payer_email") { "<MISSING_EMAIL>" }
  DB[:ipns].insert(email_address: email,
    data: Sequel.pg_json(params))

  # Prepare a single-use product redemption token
  token = SecureRandom.hex
  DB[:tokens].insert(email_address: email, value: token)

  # Prepare a welcome email
  subject = "Claim your purchase!"
  redemption_url_template =
    Addressable::Template.new("#{request.base_url}/redeem?token={token}")
  email_template = Tilt.new("templates/welcome_email.txt.erb")
  contact_email  = "contact@example.com"
  redemption_url = redemption_url_template.expand("token" => token)
  body           = email_template.render(Object.new,
    login_url:     redemption_url,
    contact_email: contact_email)

  # Send the email
  Pony.mail(to: email,
    from: contact_email,
    subject: subject,
    body: body,
    via: :smtp,
    via_options: settings.email_options)

  # Report success
  [202, "Accepted"]
end

For better or for worse, this is how a lot of web application controller actions look, especially when we haven’t yet identified all of our domain objects. Cluttered with a mix of domain and infrastructure concerns, and incorporating database access, presentation logic, and business rules.

There are a lot of responsibilities here that could be teased apart. But the application is a small one, and absent a need for frequent updates to IPN-handling functionality, achieving a “perfectly factored” design could be a lot of work for little payoff.

One area we are updating frequently, however, are the routes and controller actions (which in Sinatra are incorporated in a single file). So we’d really like to slim down our controller actions, and move the bulk of their logic somewhere else.

So where could we quickly move this mass of DB/logic/emailing code?

Over the last few years, an answer to this question has emerged in the Ruby web coding community: Service Objects.

Enter the Service Object

Here’s the code above, moved into a service object:

class IpnProcessor
  def process_ipn(params:, redemption_url_template:, email_options:)
    # Record the raw data before we do anything else
    email = params.fetch("payer_email") { "<MISSING_EMAIL>" }
    DB[:ipns].insert(email_address: email,
      data: Sequel.pg_json(params))

    # Prepare a single-use product redemption token
    token = SecureRandom.hex
    DB[:tokens].insert(email_address: email, value: token)

    # Prepare a welcome email
    subject = "Claim your purchase!"
    email_template = Tilt.new("templates/welcome_email.txt.erb")
    contact_email  = "contact@example.com"
    redemption_url = redemption_url_template.expand("token" => token)
    body           = email_template.render(Object.new,
      login_url:     redemption_url,
      contact_email: contact_email)

    # Send the email
    Pony.mail(to: email,
      from: contact_email,
      subject: subject,
      body: body,
      via: :smtp,
      via_options: email_options)
  end
end

And here’s what the controller action now looks like:

post "/ipn" do
  demand_basic_auth

  redemption_url_template =
    Addressable::Template.new("#{request.base_url}/redeem?token={token}")

  IpnProcessor.new.process_ipn(
    params: params,
    redemption_url_template: redemption_url_template,
    email_options: settings.email_options)

  # Report success
  [202, "Accepted"]
end

Apart from authentication and the HTTP status return, the only bit of code we’ve kept out of the service object is the URL template for the product redemption link. We kept that behind because it’s a routing concern, and so it belongs in the HTTP-handling layer.

Is this new object justified?

This refactoring has certainly cleaned up the controller action. But was the creation of a new class actually justified?

Take a look at the code invoking our new service object.

IpnProcessor.new.process_ipn

“IPN processor … process IPN”. That seems a little redundant.

This kind of class-name/method-name reiteration is always a red flag for me in terms of domain modeling. Sandi Metz, author of Practical Object-Oriented Design in Ruby, says to first identify the message, and then identify a fitting role to receive that message. Saying that a process_ipn message should be received by an “IPN processor” seems tautological.

It’s a bit of a cheat, in fact. We could say the same for any message: “who should receive the save message? Why, the Saver, of course! What about the increment_amount message? The AmountIncrementer!”

And let’s talk about that name, IpnProcessor. The class it names handles business logic (among other things). “IPN” is certainly business terminology. But is IpnProcessor a term in our business domain? Or is this a concept we invented solely to house a method called process_ipn?

What do we do when we can’t come up with an appropriate receiver for a message?

Procedures: Not just for C code

Take a look at the code inside the process_ipn method:

  # Record the raw data before we do anything else
  email = params.fetch("payer_email") { "<MISSING_EMAIL>" }
  DB[:ipns].insert(email_address: email,
    data: Sequel.pg_json(params))

  # Prepare a single-use product redemption token
  token = SecureRandom.hex
  DB[:tokens].insert(email_address: email, value: token)

  # Prepare a welcome email
  subject = "Claim your purchase!"
  email_template = Tilt.new("templates/welcome_email.txt.erb")
  contact_email  = "contact@example.com"
  redemption_url = redemption_url_template.expand("token" => token)
  body           = email_template.render(Object.new,
    login_url:     redemption_url,
    contact_email: contact_email)

  # Send the email
  Pony.mail(to: email,
    from: contact_email,
    subject: subject,
    body: body,
    via: :smtp,
    via_options: email_options)
end

What can we say about this code?

We can see that it doesn’t depend on any state in the IpnProcessor class.

And we it seems to walk through various steps, grabbing objects from various places, and performing a sequence of actions on those objects.

There’s a name for this kind of code: a procedure. Or, in the terminology of Martin Fowler’s Patterns of Enterprise Application Architecture, a transaction script.

Where can we put procedures in a Ruby application? Well, in my applications I usually have a module that gives the app code an overall namespace. In this case, the app was called “perkolator”, so the module was named Perkolator.

Service Object to Procedure

Let’s make the process_ipn method a public module-level method on this module:

module Perkolator
  def self.process_ipn(params:, redemption_url_template:, email_options:)
    # ...
  end
end

And update the controller action:

post "/ipn" do
  demand_basic_auth

  redemption_url_template =
    Addressable::Template.new("#{request.base_url}/redeem?token={token}")

  Perkolator.process_ipn(
    params: params,
    redemption_url_template: redemption_url_template,
    email_options: settings.email_options)

  # Report success
  [202, "Accepted"]
end

This is a pretty small change. Does it actually make any difference?

I believe it does, for a couple of reasons.

The wrong object causes more trouble than no object at all

My yard is dotted with maple seedlings. They are small and easily uprooted. But some are at the forest edge, where they will cause little trouble as they grow larger. Whereas others are in garden plots, where their root systems will eventually threaten retaining walls and other landscaping elements.

An object that handles business logic but doesn’t have a well-defined business domain role is like one of these dangerously-located seedlings. Objects tend to grow and accumulate more responsibilities. As Corey Haines puts it, objects are attractors for functionality. And once they mature, objects with confused, ill-defined roles can be some of the hardest to refactor.

Service Objects accumulate invisible inter-service coupling

Many blue circles, some of them connected by a jagged line

Business workflows form implicit, undocumented paths through service objects.

The object we created above, IpnProcessor, is likely to eventually share a /services subdirectory with lots of other Service Objects. And as much as a Service Object is (usually) an attempt to encapsulate logic, no Service Object exists in a vacuum.

For example, at some point we are likely to add a new Service Object to handle the case when a user clicks on the product redemption link sent by the IpnProcessor. We might call it ProductRedeemer.

What will these two objects share in common? Well, if nothing else, we know that they will be effectively communicating with each other via the database tokens table: one object writing new entries, the other reading off entries and marking them as redeemed. Eventually, they might also share the users table as well.

In effect, these two Service Objects, IpnProcessor and ProductRedeemer, will form two steps in a process of product purchase and delivery. But how will that relationship be represented in the codebase?

The answer is: it won’t be. A reader of the IpnProcessor code won’t have any clue where the story is continued, unless some kind soul leaves them a comment to guide them.

And this is my larger concern about the proliferation of service objects handling business rules: you can end up with a whole basket full of Service Objects, many with implicit data dependencies between them, representing business workflows that have no explicit representation. In the worst cases, these pseudo-independent objects don’t just share database tables, but also pass state to each other as data-blob hashes via the user session or via URL variables.

Domain-Driven Design

As far as I know, the origin of the Service Object idea is Eric Evan’s book Domain Driven Design (DDD). Interestingly, DDD deliberately avoids using the term “Service Objects”, instead simply calling them “Services”. In fact, in explaining Services Evans writes: “some concepts from the domain aren’t natural to model as objects”.

Some other things DDD has to say about Services are:

  • We should try first to find an appropriate domain model object to receive the functionality, before constructing a service.
  • Most services should be infrastructure services, such as “send an email”. Business domain services should be rare. This goes directly against some current trends in Ruby web application development, which advocate for every business action having its own Service Object.
  • As a corollary, infrastructure-level and domain-level services should be kept separate.
  • Domain-level services (e.g. a “transfer funds” service in a banking app) should be named with terminology which is part of a domain’s “ubiquitous language”.
  • Services should have no persistent state.

All of these guidelines are consistent with representing Services as procedures in a Ruby application.

If it walks like a procedure, and quacks like a procedure…

There is no shame in writing, or extracting procedural code. Especially in the early stages of implementing a new feature, the needed object roles and responsibilities may be unclear. Prematurely factoring code into objects can do more harm than good over the long run.

I would far rather work on an application with a collection of honest procedures, than one in which business workflows and other domain concepts are implicitly split across a directory full of Service Objects. In the latter case, I’d probably wind up de-factoring the Service Objects into procedures before being able to make any major changes.

As Martin Luther put it: “sin boldly”! Don’t let shame about maintaining object-oriented purity drive you to make objects from vague or half-formed domain concepts. If what you have is a procedure, let it be what it is, and be done with it!

Want to read more?

This article was extracted from my new, free email course entitled Lies of Object-Oriented Programming in Ruby and Rails. If it has piqued your interest, you can sign up for the full course by clicking here.

Published by Avdi Grimm

55 Comments

  1. How about:

    Sorry if I made some error, I don’t know ruby syntax.

    payerEmail = (params.fetch(“payer_email”) { “” }).asEmail() 
    payerEmail.sendIPN()

    Email seems to be the right object to send the IPN. You ask the fetch String to transform itself into an Email and then ask the Email object to sendIPN.

    I never understand those service, use cases, handlers and others general purpose objects. It seems always so… unnatural.

    Reply
  2. I’ve always been wondering why so many developers consider procedures a thing of the past, that died with introduction of OOP/FP/etc.

    They are not.

    Reply
  3. In my experience, service objects in combination with DI, allows simpler testing. How would you test your procedure without the ability to provide mocks as Email (Pony) and DB services?

    Reply
    • In accordance with the advice of the folks who invented mocks, I don’t mock what I don’t own.

      Reply
      • Thanks, I didn’t knew this. As I understand it, you don’t mock things you don’t own; so you wrap it in adapter or wrappers you own. Then you mock these. That’s not what you are doing.
        How do you test these functions?

        Reply
        • This example shows code that’s in a state which is, realistically, only testable at an acceptance or integration-level of test. That’s the point: a lot of code out there is at this level of organization.

          Several comments have had an implied context of: “well, [if you first refactor everything to have perfect separation of concerns, which is totally an ideal use of your time, then] using a Service Object will buy you some marginal testability convenience!”. Which, OK, sure. But taking the code as it is and just adding the Service Object part (without any of that other refactoring) actually makes the eventual refactoring (if it ever becomes necessary) harder rather than easier. In my experience.

          Reply
  4. If IPNProcessor was made into a “callable”, like a lambda, do you think It would be better?

    IPNProcessor.new.call(args)

    “`

    Reply
    • What aspect would that improve over a straightforward procedure?

      Reply
    • I’d normally go for something like ProcessIPN.call(args), which delegates to new(dependencies).call(args). The new/initialize method takes a hash of dependencies, usually for testing purposes.

      Reply
      • Maybe I’m missing something, but I don’t see how this is an improvement. Mind you, I’ve used the idiom you’re describing a lot, but the end result is something that is NOT a real, coherent concept gets treated like it is, and cruft then accumulates.

        Reply
        • Ok, now your point seems clearer. The issue is not the current object, but what will become of it, in the vein of Sandi Metz’s rationale that the wrong abstraction is more dangerous than repeated/ugly code.

          Reply
        • You don’t see any value in doing that because you are not injecting anything. For sure it doesn’t add any value, because that’s imperative code and not object oriented code. If you were injecting code or passing some arguments in the initializer, you would realize that doing it with an object is much more relevant/helpful.

          Otherwise yeah, doing imperative programming in object-oriented is going to looks you are building useless objects.

          Reply
          • I’ve addressed this both on other comments and in the article itself. Yes, you can easily say:

            • Step 1: Achieve perfect separation of concerns.
            • Step 2: Profit!

            My entire point in writing this article is that in many cases, step 1 is not only out of immediate reach, devoting time to it may actually be waste. And in that case, hiding a procedure behind the fig leaf of a “service object” that doesn’t represent any kind of real concept in the business domain does more harm than good.

            Thanks for your comment!

  5. But couldn’t you create a service that orchestrates the independent services?

    Reply
    • How does one service (which is defined as a stateless action) orchestrate a workflow that happens across multiple web requests?

      Reply
      • Is there a chance that you are blurring service layers with service objects? Why would a service object need to even be aware of web requests in terms of it’s operation? Wouldn’t the Http service layer inject/pass the current state into the service objects? Your procedure is essentially in the same position: How too would it deal with state between requests?

        Another point regarding referencing Fowler’s ‘transaction scripts’. To quote Martin’s page, “Organizes business logic by procedures where each procedure handles a single request from the presentation”. This implies that the procedure you have created in the same vein as transaction scripts, should handle a single request only. If that’s the case, then there is even less of a reason to use a different construct to a Service Object. Just as you might have multiple procedures being used across multiple requests, so too you could use multiple (stateless) Service Objects handling multiple requests.

        Reply
  6. You mentioned a few reservation about service objects, but I don’t see how a procedure it better in those cases:
    1. “The wrong object causes more trouble than no object at all” – replace “object” with “procedure”; doesn’t it remain true?
    2. “Service Objects accumulate invisible inter-service coupling” – procedures don’t?

    (DDD and “if it quacks like a procedure…” – fair points)

    From my experience, keeping it object-oriented and encapsulated is a big deal in terms of testing. I’m fine with adding some boilerplate code if it means easier unit tests, easier mocking, and/or easier dependency injection.

    Reply
    • I think the original quote from Metz is more like “the wrong abstraction is worse than no abstraction”. I think that’s better, because I agree a giant pile of procedures can definitely be the wrong abstraction too. But what I take from this post is more about “the wrong abstraction” — keep it SIMPLE (and sometimes apparently “non-customizable” or “non-DRY”) until you know the right abstraction. A simple procedure can be the simple answer. But if Avdi intended, or people take it, as “use procedures instead of objects”, I think that’s the wrong lesson.

      Reply
  7. How about handling:

    email = params.fetch(“payer_email”) { “” }

    instead of letting the code run with invalid data. Does it really make sense to save this data to the database without a valid email? Should an attempt to send email even be make when you know the email is invalid? Maybe error handling was removed just for this example?

    It should be stupid easy to see what is happening:

    1) save_to_db if valid
    2) build_email_body
    3) send_email

    I know there are comments that essential point this out, but I would probably drop the comments and split things up into 3 methods instead. I should be better about paying attention to comments, but for some reason the first few times I read through this code, I didn’t even realize those comments where there.

    Reply
  8. Very good points Avdi, but how do you deal with multi-method procedures? I prefer short clean procedures that are then composed into the larger procedure. These can be more granularly tested. In this case, intermediate variables could be considered state (eg. from your example: body, redemption_url, contact_email, etc.) with accessors. However, having a bunch of private methods and a single public ‘call’ method doesn’t feel right either.

    I keep thinking about the mess that the rails Helper methods turn into with a procedure approach.

    Your thoughts would be appreciated!

    Reply
  9. I’m not sure I follow… I agree that the term “service object” is a bad one as it’s too generic and you might as well call it “object that does something”.

    To me it’s just roles that expose interfaces, hopefully relevant to the role, where the role and directory names speak to what they actually do instead of the general design pattern they implement. ex. app/payment_processors instead of app/services. But yes, keep those public interfaces narrow, watch out for cruft and scope-creep. Think hard about names, etc.

    In the case of a multi-step process I’ll usually come up with an object that exposes the relevant steps

    workflow = SomeWorkflow.new(*dependencies, state, etc)
    workflow.step1(*args)
    workflow.step2(*args)
    workflow.step3(*args)

    steps might delegate to other collaborators or stateless procedures, for example to issue API requests, etc

    As to the difference between Something.new.do_something() and Something.do_something(), if the role only exposes a single public method (ie. a “procedure”) I might use your single-method-in-a-module approach. But when I still need to split internal state management I can still use instances internally, as in

    class Something
    def self.do_something(*args)
    new(*args).do_something
    end

    def initialize(*args)
    # etc
    end

    def do_something
    # etc
    end
    end

    I find that it helps thinking of .new as a factory method, not part of an object’s public API.

    As to naming the role and method, I agree that IPNProcessor.process_ipn seems confused. What about IPNProcessor.run or even IPN.process?

    Reply
  10. Nice article and mental exercise

    However in my case, it happened a lot that some services needs to share some code, via inheritance for example.

    So

    class A
    def common_code
    end
    end

    class B < A
    def execute
    common_code
    b_specific_code
    end
    end

    class C < A
    def execute
    common_code
    c_specific_code
    end
    end

    And actually it turned out to be very simple to understand and debug.

    Also regarding services dependency, if a service B will never be called except after service A, then in the controller I don’t do: A.new(args).execute; B.new(args).execute, but rather last line in A’s execute calls B’s execute

    So what we can regard as publicly exposed services can be called from anywhere(Just like a class public method), where some services will be only called from other services.

    I have written pretty complex systems, and so far the conceptual model using services is good.

    Anyway I don’t find the differences here to be that much, at the end you encapsulate some logic in a separate place, be it a class or a module, even if there some benefits of this approach over the other, they both are easy to follow and promotes good code quality.

    Reply
    • When I’m talking about coupling between services, I’m talking about coupling that’s split across multiple controller actions (and multiple requests).

      Reply
      • But then also the controller actions themselves are coupled the exact coupling, so there is no escape in my opinion.

        Reply
      • Also, the great advantage of service class is that I can define multiple private method that use the same attributes, no need to pass all attributes each time I call a method, and the main public method (execute) calls these private methods. In my opinion, this is a killing point in favoring service class over module way.

        Reply
  11. The problem isn’t about procedure vs object its about combining collation and decision making. Controllers and the things they call are collators they bring together things from disparate places (which is why they are so hard to test). Whether you write your collator as a procedure or an object you will still be vulnerable to creating something that decides and does thing as well as delegating, and that’s where the real problem lies.

    Grouping collators together is a really good idea, because they should all follow the same patterns which should be:

    delegate
    don’t decide
    be so simple that you don’t need to be tested. (integration tests will provide the coverage)

    If you follow this, then maybe then your ServiceObjects will be neat and manageable and your service procedures may well be even neater and more manageable. If you don’t then you will create a mess.

    Reply
  12. Hey Avdi,

    Really interested in this topic, btw, so thanks for the write up. I will usually do Service.new(dependencies).call instead of Service.call(dependencies) basically because it allows me to break down call into multiple private methods that have access to the dependencies without having to pass them around as arguments to all the methods. Also I have a rule of thumb that call is the only allowed public method for the object, making this object basically a function, in concept.

    Now, as I understand it you’re advising against these type of objects in the first place. My question is then, if this is something that you need to call from different places (and assuming copy & paste the controller code is undesirable), what would you do? Are you advocating for bounded contexts and aggregates? How to integrate that into rails, though?

    Thanks.

    Reply
  13. Yes! Exactly! I feel like you took the words straight out of my mind. I basically burned out in the world of programming because of my in-ability to prevent co-workers from writing code organized in such perverted ways as you describe here. “Service Objects” and the “Feature Envy” smell dominated the codebase of my last job and I spent all day just rooting them out.

    Reply
  14. Personally switching to service objects has been the best thing I’ve done recently, and it’s solves several problems:
    – Eliminates the fat model conundrum where I’m afraid to change methods in my domain model because I don’t know what it will break
    – All of the code for doing an action is encapsulated in one place
    – It’s very clear what actions my code is designed to do
    – Testing a service object is simple – test inputs, outputs, and actions performed (updating the database). I only have to test what the service object is designed to do, as opposed to actions on models where I have to think about all of the ways someone might use that part of the model.

    I realize that there are no silver bullets, but this pattern has encouraged me to write much simpler, more modular, and more maintainable code. When I work on code that doesn’t use this pattern, I find that the code the perform what a service object would do tends to be spread out throughout several classes, making it harder to test, mock things out, and understand what’s going on.

    It sounds like you’ve found some middle ground between service objects and fat models, so I’d be curious to hear your thoughts about where the line is.

    Reply
  15. Yeah. Don’t call them Service Objects… call them services! Problem solved.

    Reply
  16. Correct me if I’m wrong but the only difference I see in this example is that multiple services aka procedures would live either in separate objects (service objects) or in one module (procedures).

    Sure it reads better to do MyProject.process_order than OrderProcessor.new.process_order, but dumping everything in one file can get messy quick. I rather have 20 files with one public method than 1 file with 20 methods.

    Plus the added benefit that your “service objects” CAN have state if need be.

    Reply
    • Adding state to something that should never have been a [single] object in the first place is like handing a bucket of mud to a bored toddler. Indoors.

      That’s kind of the point of the article: if you have a mess, don’t make it easy to institutionalize and grow the mess.

      Reply
  17. In reference to “Service Objects accumulate invisible inter-service coupling,” why not just make more subfolders for related service objects inside the service objects folder. Using your example service objects: IpnProcessor and ProductRedeemer, you could put them both in a ‘/service_objects/purchases’ subdirectory to represent the relationship in the codebase.

    Reply
  18. It sounds to me like service objects, as discussed here, are failing only for a good reference implementation.

    Is this new object justified?

    This section mostly trades on the redundant naming, which is a property of the chosen implementation more than the pattern. It reads like a strawman, even if that’s not what was intended.

    Service Object to Procedure

    If the service implementation uses the .call (or .perform) pattern to solve redundant naming, then this does not change the calling interface (Module.method(args)).

    What it does do is hinder the nice factoring that you might want to do later as the complexity grows. When the service object is described by a unique constant, you can always refactor the implementation later by escaping into an execution class.

    The wrong object causes more trouble than no object at all

    I think this section is a stronger argument for using unique constant names. When objects attract functionality, there’s less friction to refactor MyProcedure.perform than MyApp.my_procedure because the execution class already exists.

    Service Objects accumulate invisible inter-service coupling

    As others have observed, this is already a property of REST. We haven’t added any coupling problems by moving logic from a controller action to a service object.

    What we have done, though, is created an opportunity to improve the coupling problems that arise when business logic is strung through an ORM with lifecycle hooks. Service objects can have excellent composability, especially when using something like promise-rb for Result-like patterns.

    If it walks like a procedure, and quacks like a procedure…

    Then it might be a good service object? 😀

    I’m a proponent of pattern minimalism, and service objects have made the cut in my playbook. As a bonus, I’ve found that the pattern translates well across other frameworks where the communities are less invested than Rails in perfecting such an active domain model.

    Reply
  19. Hi Avdi:

    Thanks a lot for your article. Since I read it I cannot stop thinkig about what I also read in the trailblazer gem (https://github.com/trailblazer/trailblazer) which says:

    “1- All business logic is encapsulated in operations (service objects).”
    “4- Models are persistence-only and solely define associations and scopes. No business code is to be found here. No validations, no callbacks.”

    which I understand is right the opposite of what Eric Evan and DDD says about service objects. Maybe trailblazer operations can be both Domain Level Objects and Service Objects?

    The thing is I find good points in both trailblazer and what you are saying abou precedures and DDD and I’m a little confused now. So, I was wondering what you think about this.

    For the record, I know very little about DDD. I’ve started to look at it after reading this article.

    Thanks!

    Reply
    • I think it sounds like I look at things a little differently from the Trailblazer team.

      Reply
    • Trailblazer is a different approach than DDD. It calls itself “the advanced business logic framework” and says that you will “[finally] know where to put your code!”.

      DDD people will tell you that “where to put your code” entirely depends on your domain. This is in stark contrast to Trailblazer that tries to give you filing cabinets for all your code whatever your business is. In that it is similar to what people saw in MVC, only the cabinets are different.

      You probably could use Trailblazer and still do DDD, but I’d question the value of going against the grain of the Trailblazer framework.
      Mind you, I’ve never used Trailblazer. It merely had piqued my interested back when it was first published and I’ve read about it. So take anything I’ve said about it with a grain of salt.

      Reply
  20. Just name em service procedures and be done with it? 🙂 Unless you start injecting dependencies. Or breaking procedures into smaller chunks just to make the code more readable.

    I’ve found having transaction scripts as classes makes whole system more “navigatable” by the developer, for example submit_batch_command.rb can easily be located and serves as clear entry point to specific business process (which transaction scripts usually implement).

    Object instances can also more easily transformed into executable messages which can be passed to some generic wrapper, would it be background processing or some other handler.

    Reply
  21. To not have a question with naming we came up with the following convention: name a service object like this <DoSomething><Subject>Service. Where DoSomething is a verb, Subject – a subject on which an action (verb) is performed. For example SignupUserService, SendEmailService and etc. This is IMHO much more readable than UserSigniper#signup_user. Each service should define only 1 method, let it be #call. Then we can write it like that SignupUserService.new(user).call. But we are lazy and went further – we agreed to transform a class into a “global” function. So a call looks like this: SignupUserService(user). May be in some cases these functions should be put into a namespace but we haven’t come up with so much code that has this problem.

    This is the implementation if you are interested: https://gist.github.com/ka8725/27321acc3d22a7d954505d974e414a9e

    Reply
  22. People who create service objects or service procedures on some enclosing namespace module always start off with good intentions. But, over time, they almost inevitably grow fuzz and more code and more complexity. Along the way, the naturally encapsulate and fossilise the structure of how the objects it interacts with connect. So, if you need to refactor how your objects inter-relate you find yourself having the refactor huge chunks of monolithic procedural code to get things working again. It is in my view a really nasty anti-pattern.

    Instead, its much better to pick a business domain object to receive a message and use that as the start of some proper OO cooperational code to get things done as a consequence of the cloud of domain objects passing notification type messages between them. That way, when you refactor your business domain objects you are naturally lead in the direction of refactoring the message passing too. WIN-WIN.

    Reply
    • Agreed Peter.

      I see guys are running down the procedural thinking missing the object oriented approach. Proper abstraction is skipped, never attempted, and as a result procedures are written that are disguised as legitimate objects calling it OO. To get this code working later is troublesome and is not object oriented.

      Service Objects are a software pattern that is nowadays abused in Ruby/Rails. It could be used when there is a real justification for it, however, there is so small occurrence that doesn’t lead to the hype that is making noise in the industry becoming the primary architecture pattern of many teams.

      There is no shame to have a procedure or start with a simple procedure. Just call it what it is. As soon as a domain object emerges it is a right time to extract it. Having procedures (Service Objects) in mind however takes thinking away of abstraction that needs to be performed.

      That quite bothers me, so many guys are confused and distracted by information they read in blogs and then bringing these habits with them to new teams and dare to fight for it, justifying it that everybody uses it.

      Quite disappointing seeing people above justifying for code that reads MyService.new(…).call(). And even taking it further to MyService(args). Calling it OO.

      Reply
      • i agree with the points. but the main issue i have with service objects is that they give you a handy place to ‘stuff complexity’ so you can pretend it does not exist.

        time passes, more requirements emerge, the service object it tweaked again and again. each time to implement some feature that is tiny. so there is no time for cleanup. and all of a sudden you have your kitchen junk drawer in code.

        now. some large change comes along requiring you to change your objects and how they connect to each other and now, the monster from the junk drawer emerges to devastate the city and eat all the peoples.

        Reply
        • Yes, this, exactly. No micro-architecture is a substitute for actual code design based on understanding the domain. I like your ‘kitchen junk drawer’ metaphor.

          What I take from avdi’s original post is, keep things as simple as possible until you actually have the understanding of how much and what design you need, and spend the time to do some actual design.

          “Pile of procedures” can easily become a kitchen junk drawer too. It’s just a simpler one with less commitment and less assumptions and less coupling, acknowledging it’s junk drawer nature, until you can commit with educated instead of reflex assumptions.

          Reply
          • You could also argue that domain models could also become massive kitchen junk drawers.

            What service objects are doing is acknowledging that objects can have many different actions done to them, and sometimes those actions are done to several domain models acting together, and it’s choosing to group code by action instead of the object. Then when you have to refactor, you know exactly which actions are affected in the application and what to test/re-test.

        • Well said and to the point. Thanks.

          Reply
          • To John, for actions on objects OO gives you methods. Abstract the object, implement its actions.

            If you have kitchen junk drawers in your domain object then continue improving your domain objects design. Don’t compromise putting the junk to different drawers where they look to be justified creeping upon you later.

            Exactly what I have been saying above, skipped thinking of further abstraction, thinking in procedures, leading to compromising oo principles and troubles John explained well. Improving design at first look difficult, procedures easy. Face the problems early, be happy later. Done.

  23. I’m wondering if you saw Brian Will’s three talks on youtube about OOP is garbage? This is the second talk:

    and it discusses cases like yours.

    Reply
    • I think I tried at one point, but it was like wading through a field of strawmen.

      Reply
    • im wathing that video.

      he starts off with his basic premis. ok

      example 1 thats another service object example. not really a good choice if your intent is to discredit the entire oo approach.

      example 2 yes the oo example of the coin flip game is aweful. but saying ‘here this is all you need is ok’ but only for this simple example. organisationally, procedural code simply does not scale well.

      example 3 the specific example is valid. but apart from the oo side using polymorphism to organise the cases, theres not really much oo there.

      example 4 my issue with this is not the code. my issue is that as part of moving it from oo to procedural, he now has a namespace issue. he can no lomger use getbool etc anywhere else. whereas the oo code naturally namespaces things.

      i have worked on large procedural code bases in the past. and take it from me. even if the only thing you take from oo is a standard namespacing architecture, that in itself would almost be enough.

      Reply
  24. Wouldn’t it be great if DBMS implementors could give us some kind of programming language or runtime so we could run our procedural transaction scripts directly in the database engine? We could take advantage of being local to the data and avoid pulling things back to the app server.

    If only someone had thought of this!

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *