DEV Community

Augusts Bautra
Augusts Bautra

Posted on

2

Use stateless services

This post is a response (comment, really) to Tim Riley's 2017 presentation on functional architecture, specifically, how to write truly stateless and functional services.

The bad example from him

Image description

This approach allows re-using the service object.
Image description

I believe the ability to do so is a negative, but even if you disagree, I will show how the reuse comes at too high a cost:

  1. The API becomes dispersed, some stuff in init, some in call. Where do I put a new parameter?
  2. Memoisation becomes impossible.
  3. Extract-method refactoring becomes cumbersome to impracticality because we have to pass everything as arguments.
def call(feed)
  # something very long
end

private

def split_out_of_call(feed)
end

def other_split(download_feed, some_loop_arg)
end
Enter fullscreen mode Exit fullscreen mode

The alternative

Let's lean into the functional aspect of a service, and disallow instantiation! This will remove the ability to reuse (an actual win in my book, YMMW), and allow us to use private instances as full black boxes:

class ImportProducts
  def self.call(...)
    new(...).call
  end

  # This is key. *Everything* about the instance is private.
  private  

  attr_reader :download_feed, :products_repo, :feed:

  # yes, even the initializer is private 
  def initialize(download_feed:, products_repo:, feed:)
    @download_feed = download_feed
    @products_repo = products_repo
    @feed = feed
  end

  # no params for #call
  def call
    # ...
  end

  def split_out_of_call
    # #feed implicitly available
  end

  def other_split(some_loop_arg)
    # #download_feed implicitly available
  end 
end
Enter fullscreen mode Exit fullscreen mode

And now in action

ImportProducts.(
  download_feed: download,
  products_repo: repo,
  feed: books_feed
)
Enter fullscreen mode Exit fullscreen mode

This way there is never an instance which might get some @errors populated to be accessed later. Everything needs to be in the #call return value, often a hash, but ideally a Struct.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (2)

Collapse
 
katafrakt profile image
Paweł Świątkowski • • Edited

Your proposal on how to write service classes makes a lot of sense (I use this approach often as well). However, I'm not sure I agree that it's inherently better than what Tim suggested. And leaving better/worse debate aside, I'd say this way the services are actually less functional.

To your points:

The API becomes dispersed, some stuff in init, some in call. Where do I put a new parameter?

I think it's pretty simple distinction, really. Data as opposed to dependencies/infrastructure. When a new param comes, you just have to figure out which it is. This might actually lead to less convoluted APIs, if you have a lot of dependencies.

Memoisation becomes impossible.

That's good, actually. Memoization is overused in Ruby, leading to subtle errors. Most of the time you don't really need it, and if you do, you can overcome this limitation (for example with introducing another class, which is not a service class).

In any case, memoization is not really a functional thing, as it relies on an internal state of an object.

Extract-method refactoring becomes cumbersome to impracticality because we have to pass everything as arguments.

The argument-passing argument is true, can't disagree. However one might say that if that's a problem, perhaps you're not modeling enough (in functional terms). I mean, relying on object's internal state (but making it read-only) just to save some typing always felt quirky for me.

This way there is never an instance which might get some @errors populated to be accessed later.

True again, but also not very functional approach to populate classes internal state instead of putting error in returned value. I think both you and Tim would frown upon the private method just populating an instance variable during execution and something external relying on that.

That being said, I just stress again that I think both your approaches are good, just focus on slightly different aspects. And in the end it's just important to weigh the trade-offs and choose the one that works better for you.

Collapse
 
epigene profile image
Augusts Bautra •

Thank's for the comment, Paweł. You are right, of course - there are no solutions, only trade-offs.

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up