Implementing a State Machine as a service with aasm gem.
Tags: ruby, patterns
Published: 2020-08-06 00:27:29

I recognise the [gem aasm](https://github.com/aasm/aasm) is one of the most useful gems when we want to explain how our objects or logic will change among states and which conditions will be. However, after many years I’ve seen how many developers are still shooting themselves with this gem.

The original code has been done in collaboration with [Martin Madsen](https://dev.to/martinbjeldbak), the code present in this blog is a generalization from the original, still, it is a similar concept.

I’ve seen with this gem is how they use the ActiveRecord implementation for every case. Let’s be clear, this implementation is great when the state and dependencies related are only models and you are not creating circular dependencies.

Multiple times we are working with the next scenario:

* We have a booking for a room when the user is starting this booking its state is **draft**.
* Later on, the user is confirming the date we will change it by **pre-reserved**.
* When the payment has been done the state will be booked.
Finally, when the booking has been fulfilled, the status will be **fulfilled**.

Of course, we might have more transitions between those cases, but with just 4 this example will be enough.

## ActiveRecord Implementation

Usually, your code if you are implementing the ActiveRecord approach your code it will look

```rb
class Booking < ApplicationRecord
  include AASM
  belongs_to :provider
  belongs_to :client
  aasm do
    state :draft, initial: true, after: :notify_draft
    state :pre_reserved
    state :booked, after: :notify_reservation
    state :fulfilled
    event :pre_reserve! do
      transitions from: :draft, 
        to: :pre_reserved, 
        after: :notify_draft
    end
    event :book do
      transitions from: :pre_reserved,
        to: :booked,
        after: :process_book
    end
  end
  
  def notify_draft
    Bookings::Email.send(provider, 'Someone is interested')
  end

  def process_book
    Bookings::Payments.release(self)
    Bookings::Email.send(client, 'your reservation has been done')
    Bookings::Email.send(provider, 'you got a new reservation')
  end
end
```

### Pros and Cons

* Booking model is prone to become a [god object](https://refactoring.guru/es/smells/large-class).
* [Single responsibility principle](https://en.wikipedia.org/wiki/Single-responsibility_principle) is being violated. After changing states we are adding business logic. Booking is responsible of persistency, changing states(we can consider this as persistency if we don’t want to be strict), sending emails -when sending emails-, and releasing payments.
* Testing will become harder, more items to stub.
* Wrong dependency direction. Entities now will know about other services. Therefore, more dependencies.

## State machine as a service.

A great approach is implementing the state machine as a service and injecting the booking we want to modify.

```rb
module Bookings
  class StateService
    include AASM
    attr_reader :booking
    
    def initialize(booking)
      @booking = booking

      # we need to initialize the state machine base on the current booking
      aasm.current_state = booking.status.to_sym
    end

    aasm do
      state :draft, initial: true, after: :notify_draft
      state :pre_reserved
      state :booked, after: :notify_reservation
      state :fulfilled

     # This callback could be something different, will dependent of your code
     after_all_transitions :save_state
      event :pre_reserve! do
        transitions from: :draft, 
          to: :pre_reserved, 
          after: :notify_draft
      end

      event :book do
        transitions from: :pre_reserved,
          to: :booked,
          after: :process_book
      end
    end
  
    def notify_draft
      Bookings::Email.send(provider, 'Someone is interested')
    end

    def process_book
      Bookings::Payments.release(self)
      Bookings::Email.send(booking.client, 'your reservation has been done')
      Bookings::Email.send(booking.provider, "you've got a new reservation")
    end

    def save_state 
      booking.update_attributes!(status: aasm.to_state) 
    end
  end
end
```

To achieve this implementation we only need to be aware of 2 sections. Everything else is your own code

The first piece of code is your initialiser, you will need to indicate what is the current state for you machine, your instance should have the value. Don't let you misguide you as a default value, it is the value for your entity.

```rb
def initialize(instance)
 aasm.current_state = instance.status.to_sym
end
```

Also when your changes are done, you need to make sure you will make it persistent, it is not mandatory this hook, but saving is a must.

```rb
aasm do
  after_all_transitions :save_state
end

def save_state 
  instance.update_attributes!(status: aasm.to_state) 
end

```

### Pros

Considering this approach we will get two huge benefits:

* Booking model is responsible only data persistency
* This StateService is our actual State machine and the dependency direction is proper, because is a service knowing about other services, and the model booking won’t know about Email or Payments