Skinny Models and Skinny Controllers with the state_machine gem

Recently we have been working on a project to create a payment system using Stripe.  When you are processing payments you often have a process that goes through a number of steps.  For example,  if you are processing a payment for a online service that you are signing up for you might need to create a local user account,  register the user with stripe,  process a charge and finally update the local user account to put them in a paid state.  Along the way you also will have a number of failure scenarios that could break things.

At first glance you might try solving the problem procedurally using a fat model skinny controller approach.  Your code might look something like this.

 


class PurchaseController < ApplicationController
  def create
    @new_purchase = Purchase.new params
    @purchase_result = @new_purchase.process_payment
  end
end

class User < ActiveRecord::Base
  has_many :purchases
end

class Purchase < ActiveRecord::Base
  belongs_to :user
  attr_accessor :params

  def initialize(init_params)
    params = init_params
  end

  def process_payment
    user.stripe_customer_id = create_customer[‘id’]
    execute_charge 
  end

  def execute_charge
    charge = Stripe::Charge.create(
      amount: price_in_cents,
      currency: 'usd',
      customer: user.stripe_customer_id,
      description: description
    )

    stripe_charge_id = charge["id"]
    amount = charge["amount"]
    stripe_created_at = Time.at(charge["created"].to_i)
  end

  def create_customer
    response = Stripe::Customer.create(
        email: params.email,
        card: params.token,
        description: params.id
      )
  end

  def set_customer_information
    c = Stripe::Customer.retrieve(user.stripe_customer_id)
    user.card_fingerprint = c["active_card"]["fingerprint"]
    user.card_type = c["active_card"]["type"]
    user.card_last4 = c["active_card"]["last4"]
    user.card_exp_month = c["active_card"]["exp_month"].to_i
    user.card_exp_year = c["active_card"]["exp_year"].to_i
  end
end

The problem is that you have a class that violates rule of single responsibility. Your model is persisting information about a purchase, and processing payments on Stripe. We could easily refactor this code to move the stripe charge activities to another class.


class PurchaseController < ApplicationController
  def create
    @new_purchase = Purchase.new params
    @purchase_result = @new_purchase.process_payment
  end
end

class User < ActiveRecord::Base
  has_many :purchases
end

class Purchase < ActiveRecord::Base
  belongs_to :user
  attr_accessor :params

  def initialize(init_params)
    params = init_params
  end
end

class StripeProcessor
  def self.process_payment(purchase)
    user.stripe_customer_id = create_customer(purchase)[‘id’]
    execute_charge(purchase) 
  end

  def execute_charge(purchase)
    charge = Stripe::Charge.create(
      amount: purchase.price_in_cents,
      currency: 'usd',
      customer: purchase.stripe_customer_id,
      description: purchase.description
    )

    purchase.stripe_charge_id = charge["id"]
    purchase.amount = charge["amount"]
    purchase.stripe_created_at = Time.at(charge["created"].to_i)
  end

  def create_customer
    response = Stripe::Customer.create(
        email: params.email,
        card: params.token,
        description: params.id
      )
  end

  def set_customer_information(user, customer_id)
    c = Stripe::Customer.retrieve(customer_id)
    user.card_fingerprint = c["active_card"]["fingerprint"]
    user.card_type = c["active_card"]["type"]
    user.card_last4 = c["active_card"]["last4"]
    user.card_exp_month = c["active_card"]["exp_month"].to_i
    user.card_exp_year = c["active_card"]["exp_year"].to_i
  end
end

That slimmed down purchase. Now we have another problem. Somewhere between create_customer and execute charge something blows up. If you have a small system this may not be a big deal, but what happens if your site needs to handle a lot of traffic? You could add in a bunch of exceptions and a bunch of logic to catch everything.

The problem is that your code is going become difficult to follow, and tracking the state of charges is going to be a bit of a challenge as the number of charges gets larger.

When we process a purchase we create a customer and execute a charge. During a successful charge they system is actually going through a series of states. pending, creating_customer, executing_charge, processed. If there is an error we need to have a state to tell us that an error happened….so now we have pending, creating_customer, executing_charge, processed, and error. In a payment processing system we want to persist the states as we work through the steps so that we can go back to a charge and see what happened. Was the charge successful? Did if error or or get stuck in a state?

A state machine can help us to clean up the flow and model the system based on these states. Ruby has a few gems to make implementing a state machine easier. Let’s see how our code looks with the state_machine gem.


class Purchase < ActiveRecord::Base
  belongs_to :user
  attr_accessor :params

  def initialize(init_params)
    params = init_params
  end

  state_machine :state do

    after_transition :pending => :create_customer, :do => :perform_create_customer
    after_transition :creating_customer => :executing_charge, :do => :perform_execute_charge

    event :create_customer do
      transition :pending => :creating_customer
    end

    event :execute_charge do
      transition :creating_customer => :executing_charge
    end

    event :finish_charge do
      transition :executing_charge => :processed
    end

    event :create_customer_error do
	transition :creating_customer => :error
    end

    event :execute_charge_error do
       transition :executing_charge => :error
    end
  end

  def perform_create_customer
	customer = StripeProcessor.create_customer self

	if customer[:status] == :success
	  execute_charge
	else
	  create_customer_error
	end
  end

  def perform_execute_charge
      charge = StripeProcessor.execute_charge self

      if charge[:status] == :success
        finish_charge
      else
       execute_charge_error
      end     
  end
end

Now we have a very traceable flow of events through the system, but we just added a lot of logic back into the model giving it more than one concern. What you just ran into is one of the biggest problems with the current state_machine gems in Ruby. If you want to persist the state of a state machine, and use these gems, there is no easy way to inject the model into the state machine to cleanly separate the two. This leaves you with two choices. One, roll your own with a skinny model, or two use the existing one. In our case we decide to keep the current gem instead of creating our own state machine framework, or using the traditional Gang of Four class based state machine.

This doesn’t mean that we can’t clean up our model. In our case we pulled our state machine into a module and mixed it in to the model.

  
  module AccountStateMachine
    def self.included(base)
      base.class_eval do

	 state_machine :state do
           …
	 end

      end
    end 
  end

  class Purchase < ActiveRecord::Base
	include AccountStateMachine

       …
  end

You may notice a funky syntax in the AccountStateMachine module using class_eval. That is because the state machine gem is turning the dsl into methods when the class/module is being loaded and binds to the object it was defined in under the DSL. Because of this, when you require the module with the dsl, the state machine dsl will make modifications against the object context it is in, but not include these methods in the object we are trying to include into. The base.class_eval in the included method gets us around that limitation.

It’s not as loosely coupled as injecting the model into the state machine, but for us it seemed like a reasonable compromise.

Special thanks to the development team at PlayonSports for helping me to flesh out this idea.

About Me: I am a Atlanta based, native Android/IOS developer with AngularJS/Ruby experience and am founder of Polyglot Programming Inc.. Many of my projects focus on IOT and Wearables. You will often find me purr programming and I regularly speak at conferences around the world. I am available for hire! More Posts

Follow Me:
TwitterLinkedInGoogle Plus

I am a Atlanta based, native Android/IOS developer with AngularJS/Ruby experience and am founder of Polyglot Programming Inc.. Many of my projects focus on IOT and Wearables. You will often find me purr programming and I regularly speak at conferences around the world. I am available for hire!

Posted in Architecture, Development, rails, ruby, Stripe Tagged with: , , , ,

Leave a Reply

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

*