There's more than one way to build a cart

You know when you want to buy something online and you click a button to "add to basket" or something along those lines? I've been thinking about this process pretty extensively lately, and I've built a number of different carts. I'd like to think that now I know a few advantages and disadvantages to some different styles of cart. Consider this post a primer.

The Session Hash Cart

This is a very simple way to build a shopping cart and utilizes the Rails session hash. The idea is that you can add a new hash to the session session[:cart] = {} and you can then add items by id and quantity. An add to cart method might look something like this:

def add_to_cart(item, quantity)  
  session[:cart][item.id] = quantity
end  

In my opinion, this falls into the category of "cute" programming. It utilizes a part of rails in an interesting and different way, but isn't my recommended shopping cart technique.

This method is fast, clean, and simple. Depending on your requirements, it might be a good solution.

The cons outweigh the pros. The fact that this technique relies on the session hash means that it will get reset frequently. Many users will expect a cart to persist between visits and across devices. You will also still need a separate order class.

The verdict for me is that this is interesting, but not actually useful.

The Standalone Cart Model

This technique requires building a new model for your cart. It would probably be tied to a particular user or current_user. You could then add items and quantities. If necessary, you could add and access other cart attributes like the price at the time the suer added an item to the cart or the time something was added or removed.

class Cart < ActiveRecord::Base  
  belongs_to :user
  has many :items

  def add_item(item)
    self.items << item
  end

  def remove_item(item)
    self.items.delete(item)
  end
end  

This method is very good for separation of concerns, but is not very dry. This sort of cart overcomes the problems of the last method, in that authenticated users can always retrieve their carts. Amazon has trained us to expect our carts to behave a certain way.

The problem I have with this kind of cart is that it isn't DRY once you add in some kind of order model as a part of your checkout process. An order will necessarily be pretty similar to a cart with one difference.

The Order-As-Cart

This is my favorite method. You basically build an order model, add items to an order, and use a state machine to keep track of the whole business. It's clean, DRY, and slick. Here's an example of how the model might look:

class Order < ActiveRecord::Base  
  include AASM

  belongs_to :user
  has_many :order_items
  has_many :items, through: :order_items

  aasm do
    state :cart, :initial => true
    state :ordered
    state :completed

    event :order do
      transitions :from => :basket, :to => :ordered
    end

    event :complete do
      transitions :from => :ordered, :to => :complete
    end
  end

  def add_item(item)
    self.items << item
  end

  def remove_item(item)
    self.items.delete(item)
  end

This example uses the excellent aasm gem to handle the state machine duties, but those can be the topic of another post. The advantage is that this one module takes care of both cart and order duties. When the user checks out, her cart simply changes from a cart to an order. This cuts down on repeated code and is generally simpler.

This type of solution would also be very easy to build without a state machine. You would just add a status column to your order model and then recreate the needed state-change methods. I would also recommend adding in scopes to easily access orders in different states.

Extenuating Circumstances

Sometimes the order-as-cart doesn't make the best solution. I recently worked on a project where users needed to be able to make a contribution to a loan. That is very different than a normal item/cart scenario so we build a "cart" that could handle those specific needs. As usual with programming, figuring out your problem is half the problem