Single Table Inheritance (STI) in Rails

Updated: 

In this lesson, we look at the pros and cons of Single Table Inheritance (STI) in Ruby on Rails applications.

This lesson is from Full Stack Rails Mastery.

In the ecommerce marketplace app we build as part of our Ruby on Rails course, I structured the model relationships between Users, Products and Purchases like this:

class Product < ApplicationRecord
  belongs_to :seller, class_name: "User"
  has_many :buyers, through: :purchases, class_name: "User"
  has_many :purchases, dependent: :destroy
end

class User < ApplicationRecord
  has_many :products, foreign_key: :seller_id, dependent: :destroy
  has_many :purchases, foreign_key: :buyer_id, dependent: :destroy
end

class Purchase < ApplicationRecord
  belongs_to :product
  belongs_to :buyer, class_name: "User"
end


One User model is used for two types of users - buyers and sellers.
One of my students emailed me a couple of days ago to ask why I had designed it like this.
Leo asked me, "Why didn't you use Single Table Inheritance (STI)?"
Using STI is definitely an option here. I did not use it because it would mean that one user could not be a buyer and seller at the same time. They would have to create two separate accounts to buy and sell.
Let me sketch out how STI would work.
Instead of using a single User model for both buyers and sellers, we would use STI to create separate Buyer and Seller models that inherit from User.
We'd need to add a type column in the users table to store the name of the class ("Buyer" or "Seller") for each record.
All user data would still be stored in a single users table. The type column differentiates between buyers and sellers. Rails automatically sets the type when you create a Buyer or Seller.
This is what the models would look like:
class User < ApplicationRecord
  # Common user attributes and methods
end

class Buyer < User
  has_many :purchases
  has_many :products, through: :purchases
end

class Seller < User
  has_many :products
end

class Product < ApplicationRecord
  belongs_to :seller
  has_many :purchases
  has_many :buyers, through: :purchases
end

I'm not a huge fan of this approach. We're adding more complexity and limiting what a user can do, without much benefit.
It is likely that as the complexity of the app grows, having two different types of users represented by one model would become messy.
If we want separate representations for buyers and sellers, a better approach would be to create totally new models for them with one-to-one relationships with the User model:
class User < ApplicationRecord
  has_one :buyer
  has_one :seller

  delegate :purchases, to: :buyer, allow_nil: true
  delegate :products, to: :seller, allow_nil: true
end

class Buyer < ApplicationRecord
  belongs_to :user
  has_many :purchases
  has_many :products, through: :purchases
end

class Seller < User
  belongs_to :user
  has_many :products
end

class Product < ApplicationRecord
  belongs_to :seller
  has_many :purchases
  has_many :buyers, through: :purchases
end

class Purchase < ApplicationRecord
  belongs_to :buyer
  belongs_to :product
end

Now we can keep common attributes and methods in the User model and move role-specific code to the Buyer and Seller models. Also note, we can delegate role-specific logic to those classes.