The Decree Design Pattern

Majesties! Cease groveling before your codebase, and issue your applications Decrees! A Decree is a pattern designed to aid developers in extracting processes to places where they can be usefully reused.

A class that implements the decree pattern has several benefits: it can easily be replaced as the API surface area is a single method; it can compose other Decrees; it can easily be faked for development; and it can easily be mocked for testing. The Decree pattern helps to both isolate and centralize the core logic of your app.

In Rails apps, service objects are pretty much any object that’s not a controller or model. A service object can follow the Decree pattern by having names that tell our code what to do (it’s imperative to do this) and a single class-level entry point called .(...).

A few examples of imperative names are CreateObject, GenerateReport, and PerformMentalGymastics. Names like these, combined with the shorthand #call() syntax, give us very nice ergonomics compared to other naming and calling conventions. GenerateReport.(params) is much nicer to read than ReportGenerator.new(params).generate_report.

In the method signature, ... represents any set of arguments, keyword arguments, and a block. Each of those arguments are optional to define as part of the signature.

The Decree’s entry point method defined at the class level as def self.call(...). Thus, the code to execute a decree is DoSomething.(params). A Decree is useful for anything in the space between responding to a controller action and writing to the database. It is where the logic that is central to your system goes.

A simple example of a decree whose job is to create a new player being used from a Rails controller:

class PlayersController < ApplicationController
  def create
    redirect_to CreatePlayer.(player_params)
  end
end

class CreatePlayer
  def self.call(params)
    player = Player.new(params)
    player.save!
    player
  end
end

What is a Decree?

More formally, a Decree is an object with an imperative name and a single point of entry that is the same signature (or, at least, share the same method name as entry point) as all other Decrees in a system. The decree pattern incorporates elements of several Gang of Four patterns: Command, Facade, and Mediator. It is a flavor of Tell, Don’t Ask that accepts that in libraries such as ActiveRecord, what we think of as a data object is already also responsible at minimum for data validation and persistence and querying of the database.

A Decree does not have public instance methods, define getters or setters, or have a life-cycle that exceeds the unit of work being done. If there is a result that needs to be returned as a result of issuing a Decree, it should be returned from the .() entry point method. This is a bit of “functional language feature envy” that Ruby sometimes shows, and that’s a good thing! We have a class method, pass it some objects, and optionally get a result back.

Is a Decree better than a method?

I’m sure you’ve noticed that the imperative naming reads a lot like method definitions. Decrees are structured as classes rather than as methods because doing so lends flexibility. It is sometimes desirable to have the option to delegate from the class method call to an instance by internally calling new.call. Doing so provides the option to cleanly splitting work between private methods without the need to pass state around as arguments to other class or module methods.

Back in 2017, Avdi Grimm wrote “Enough with the Service Objects Already”, an excellent critique of the redundant naming I mentioned earlier. His example, IpnProcessor.new.process_ipn, is refactored to a module where Grimm likes to gather all procedures. He mentions that he tends to use a module named after the app he’s using, so he ends up with Percolator.process_ipn. This is very similar to a Decree, but rather than extracting a class which can later be refactored to have state, split work among helper methods that use that state, etc., he ends up with his module-namespaced method. In my opinion, Decrees are a similar, but slightly improved, solution to the same problem.

The pattern I usually use for “delegating to instance” looks like:

class ProcessPayment
  def self.call(payment_method:, amount:)
    new(payment_method:, amount:).()
  end

  # initalize and call need to be public methods for the class method to call
  # them, but this is a language limitation and they should not be used
  # directly.
  def initialize(payment_method:, amount:) # :nodoc:
    @payment_method = payment_method
    @amount = amount
  end

  def call() # :nodoc:
    # do the work, call private methods, return a value or don't
  end

  private

  attr_reader :payment_method, :amount
  # ...
end

Why use Decrees?

Decrees provide benefits to easy refactoring thanks to their single method API surface area, ease of composition of other Decrees in an ergonomic way, and are very easy to mock or fake.

A refactor to replace a service object can be very difficult. Service objects that don’t follow the decree pattern likely have some mix of data and logic concerns, may be long-lived, and have multiple methods as part of its public API. A Decree, by contrast, can be replaced very easily because it fully encapsulates its API into one method. Its life cycle ends when the work it’s asked to do has been completed, even if it uses the delegate to instance pattern I described. In theory, a Decree without arguments could even be replaced with an empty lambda: -> () {}. Even more complex Decrees with multiple arguments and that return meaningful data remain simple to replace as there is only that single method that needs to be modified at each call site.

Composing Decrees reads as a list of instructions.

class MakePurchase
  def self.call(customer:, product:)
    order = Order.create!(customer: customer, product: product)
    result = ProcessPayment.(payment_method: customer.payment_method, amount: order.cost)
    if result.declined?
      SendDeclinedEmail.(order: order)
    else
      FulfillOrder.(order: order)
    end
  end
end

MakePurchase.(customer: customer, product: product)

Above, it’s perfectly fine to mix Decrees with ActiveRecord, but Order.create! could also be a CreateOrder Decree as in the ProcessPayment, which is probably creating some ActiveRecord object and returning that as its result. I consider reaching for a Decree over directly creating something when I need to touch multiple objects, anytime I’d otherwise reach for a callback, and certainly any time creating requires multiple steps.

Faking a Decree is easy because you can replace the decree with a decree that does less, faster. A classic example of an opportune time to fake a bit of code is when you don’t want to make real API calls in a development or test environment. You can create a Decree that wraps that API call and any logic around it and use that in those environments rather than the real thing. If your application makes use of dependency injection, you can even fake a decree with a lambda that returns a minimal required value, either static or based on inputs.

processor = if Rails.env.production?
  ProcessPayment
else
  ->(payment_method:, amount:) { PaymentResult.new(declined?: false) }
end
MakePurchase.(customer: customer, product: product, processor: processor)

Mocking is also easy because there is only one method to mock. For example to mock ProcessPayment in rspec-mocks:

allow(ProcessPayment).to receive(:call)
  .with(payment_method: customer.payment_method, amount: order.cost)
  .and_return(instance_double(PaymentResult, declined?: false))

Or in Mocktail with:

Mocktail.replace(ProcessPayment)
stubs { ProcessPayment.(payment_method: customer.payment_method, amount: order.cost) }
  .with { PaymentResult.new(declined?: true) }

Mocking like this allows you to avoid the need to use Webmock, VCR, or otherwise fake API responses. You’ll probably still use tools like that when testing ProcessPayment directly, but there’s no need to duplicate all of that work when testing its collaborators.

Give it a shot!

Liberal use of Decrees allow us to achieve skinny controllers and skinny models. It pushes the behaviors that are most important to your application into the service layer, allowing Rails to fulfill its core competencies: handling web requests and persisting data. It improves our ability to test the most important logic in the system, avoids most naming disputes, rewards dependency injection, follows the Law of Demeter, and encourages adherence to the Single Responsibility Principle.

Webmentions

calthomp

I've been using this pattern of naming and structuring service objects over the past ~year. It's initially been greeted with some uncertainty and skepticism when I introduce it to new developers, but it tends to grow on folks as they give it a shot and look at how it's already been used.

I wrote this up partially to codify some of how I've been explaining it to folks ad-hoc, but also to share with the broader community and get input on this way of extracting processes in Ruby/Rails projects.

calthomp

I've been using this pattern of naming and structuring service objects over the past ~year. It's initially been greeted with some uncertainty and skepticism when I introduce it to new developers, but it tends to grow on folks as they give it a shot and look at how it's already been used.

I wrote this up partially to codify some of how I've been explaining it to folks ad-hoc, but also to share with the broader community and get input on this way of extracting processes in Ruby/Rails projects.

calthomp

I’ve been using this pattern of naming and structuring service objects over the past ~year. It’s initially been greeted with some uncertainty and skepticism when I introduce it to new developers, but it tends to grow on folks as they give it a shot and look at how it’s already been used.

I wrote this up partially to codify some of how I’ve been explaining it to folks ad-hoc, but also to share with the broader community and get input on this way of extracting processes in Ruby/Rails projects.

armahillo

# initalize and call need to be public methods for the class method to call# them, but this is a language limitation and they should not be used# directly.

Not sure if you knew this, but this isn't completely accurate -- it's a quirk of the language but not a limitation.

Method visibility within a class is pretty much only constrained about directly sending the message to the object; you can always pass the message in via :send -- ie:

$ irb
2.7.7 :001 > class PrivateNew
2.7.7 :002 >   def self.factory 
2.7.7 :003 >     self.send(:new, 'hello world') 
2.7.7 :004 >   end 
2.7.7 :005 >   private 
2.7.7 :006 >   def initialize(msg) 
2.7.7 :007 >     puts msg 
2.7.7 :008 >   end 
2.7.7 :009 > end => :initialize 
2.7.7 :010 > s = PrivateNew.factory 
hello world 
=> #<PrivateNew:0x00000001282c4290> 
2.7.7 :011 >

So in your example, you could do this instead:

class ProcessPayment
  def self.call(payment_method:, amount:) 
    self.new(payment_method:, amount:).send(:call) 
  end
private 
  def initialize(payment_method:, amount:) # :nodoc: 
    @payment_method = payment_method 
    @amount = amount 
  end
  def call() # :nodoc: # do the work, call private methods, return a value or don't 
  end
  attr_reader :payment_method, :amount
  # ...
end

That should accomplish the same effect while also discreetly hiding away the methods that shouldn't be exposed.

This is neat, thanks for sharing. I think I still prefer Avid's method, but TIL some new ways to invoke :call. :)

markrebec

One of the first things I plug into any new or inherited rails project over the last few years is ActiveInteraction.

It encourages imperative naming, provides a similar pattern (but one that I actually like a bit better than these Decrees personally), has a ton of stuff built-in for handling input definitions, validations, composition, etc. and has a few niceties (like error handling) that are fully compatible with ActiveSupport/ActiveRecord.

Agree 100% on the benefits of this pattern - re-use, refactoring, faking/mocking... you can even speed up a large/slow test suite significantly if you strategically and intentionally mock nested or composed interaction calls.

elementboarder

His example, IpnProcessor.new.process_ipn, is refactored to a module where Grimm likes to gather all procedures. He mentions that he tends to use a module named after the app he’s using, so he ends up with Percolator.process_ipn. This is very similar to a Decree, but rather than extracting a class which can later be refactored to have state, split work among helper methods that use that state, etc., he ends up with his module-namespaced method. In my opinion, Decrees are a similar, but slightly improved, solution to the same problem.

As a thought exercise, how would you respond to someone asserting that this is premature optimization?

I think a lot of ruby developers are against the idea of having multiple module methods that take the same arguments, but I'm not super convinced that that is so terrible. If a module method does end up growing and becoming more complex, I don't think its horrible to just split it into 2 or 3 module methods that call each other up to a certain point. I think it's easier to go from module method -> multiple module methods -> service class as the complexity grows. Obviously this all depends on the complexity of the procedure at hand, and I'm not arguing that we go back to procedural code with tons of functions that take the same arguments.

These are just some thoughts that I'm not super convinced of but I find myself reading a lot of blog posts about how to finally fix the service objects problem and it never feels quite resolved so I'm always looking for new ideas on how to go from simple procedure -> medium complexity procedure -> full blown OOP

Weird_Suggestion

Do you define a Decree as a Ruby class that implements only one .call class method? For some, this is the definition of a service object. Are there any other differences?

It's the first time I see an article using the only advantage I see of service objects. They share the same interface and can be swapped to compose classes. Nice.

I admit I'm not a fan of service objects at all. I wrote an article back then about writing better service objects, which I think the Decrees you currently describe could benefit from. I find Service objects problematic for other reasons than being single global functions.

Weird_Suggestion

Do you define a Decree as a Ruby class that implements only one .call class method? For some, this is the definition of a service object. Are there any other differences I’ve missed?

It's the first time I see an article using the only advantage I see of service objects. They share the same interface and can be swapped to compose classes. Nice.

I admit I'm not a fan of service objects at all. I wrote an article back then about writing better service objects, which I think the Decrees you currently describe could benefit from. I find Service objects problematic for other reasons than being single global functions.

Esparta :ruby:

@caleb just yesterday I was listening to the Bike Shed podcast, and they were talking about exactly that pattern:

bikeshed.fm/358

I've been working with that pattern for a while, and it's good, the testing/mocking area is perfectly aligned and testable. When is time to integrate multiple Decrees I tend to propose evolve that pattern to include the Do-notation and we are going great at this point.

Some other very similar is what TrailBrazer framework does with their Operations.

358: Class Methods
honeyryderchuck

This way of doing service objects needs to stop. Obfuscating the creation of the objects, where arguments become ivars, I mean. First, it needlessly allocates an object l, thereby increasing gc pressure, when you only need a function. Second, arguments as ivars are a terrible idea. If you typo you blow with a "variable fool not found". If you typo an ivar, you get a "no method error for nil" error. Moreover, .call to new(args).call is just needless boilerplate in the way of your business login. Just use functions. If you want to segregate, put it in a module function. Don't plan for the time you'll eventually need state, just yagni.

honeyryderchuck

This way of doing service objects needs to stop. Obfuscating the creation of the objects, where arguments become ivars, I mean. First, it needlessly allocates an object, thereby increasing gc pressure, when you only need a function. Second, arguments as ivars are a terrible idea. If you typo you blow with a "variable fool not found". If you typo an ivar, you get a "no method error for nil" error. Moreover, .call to new(args).call is just needless boilerplate in the way of your business logic. Just use functions. If you want to segregate, put it in a module function. Don't plan for the time you'll eventually need state, just yagni.

aithscel

or rediscovering the benefits of functional programing in an obfuscated way...

as others said:

Why not just use lambdas or funcs ?

keel_bright

Curious, where does the name for this pattern come from? I haven't seen it before.

We use a very similar pattern to this at my work. One benefit I've incidentally found is additional flexibility in testing by allowing you to inject or not-inject your dependencies depending on your needs.

For example, say we've got a basic function that reads data

``` class DoSomething def initialize(data_source: CoolDataSource) @data_source = data_source end

def self.call new.call end

def call data = @data_source.getData() ## do stuff with data here end end ```

Now, because I have defaulted the parameter in the constructor, I have different ways to call the function given your test type.

If I want to unit test this Service Object. I can do DoSomething.new(data_source: mock_data_source).() which allows me to test different expected outputs for CoolDataSource.getData().

Then, if I want to do a bit more of an integration test, I can just use DoSomething.() which will just use the actual dependency.

keel_bright

Curious, where does the name for this pattern come from? I haven't seen it before.

We use a very similar pattern to this at my work. One benefit I've incidentally found is additional flexibility in testing by allowing you to inject or not-inject your dependencies depending on your needs, similar to doing things OO, in addition to effectively making functions first-class citizens.

For example, say we've got a basic function that reads data

``` class DoSomething def initialize(data_source: CoolDataSource) @data_source = data_source end

def self.call new.call end

def call data = @data_source.getData() ## do stuff with data here end end ```

Now, because I have defaulted the parameter in the constructor, I have different ways to call the function given my test type.

If I want to unit test this Service Object. I can do DoSomething.new(data_source: mock_data_source).() which allows me to test different outputs for CoolDataSource.getData().

Then, if I want to do a bit more of an integration test, I can just use

DoSomething.()

which will just use the actual dependency.

mashatg

If this is has to be an outcome of eleven years of developers experience, then it is a clear sign of a decline. So sad…

postmodern

@caleb that doesn't exactly fit my usecase, where I want to define "Worker" type classes that return or yield additional information based on the input value. These "Worker" classes will be initialized and called multiple times. They represent more of a strategy or technique for getting information, than an imperative action.