Building a Subscription Billing System From Scratch with Rails and Stripe

Jake Marsh
October 02, 2019

As an early-stage startup building a paid product, there’s an entire feature-set you’ll inevitably have to build out: a billing system. Although this can (and should) be extremely basic in your first iteration, some time and thought should still be taken to ensure it’s done correctly and you’re set up for success moving forward. The last thing you want to be debugging or struggling to scale is your ability to take people’s money.

Luckily, there are a huge number of APIs and tools that make building out a simple billing system fairly straightforward. Our personal favorite is Stripe. Not only do they offer billing APIs and dashboards for likely everything you could need, but their documentation and developer ecosystem is also known for being extremely good. This is all not to mention their suite of services to help early startups like Atlas, or their developer tools like Sorbet.

Monolist is the command center for engineers — tasks, pull requests, messages, docs — all in one place. Learn more or try it for free.

We’re going to be diving in and building a simple billing system from scratch. Since we use Rails here at Monolist and it’s fairly easy to get started with, that’s what we’ll be working with. Per our reasons above, we’ll be using Stripe Billing. There will also be a focus on resiliency to ensure we’re handling all possible cases that Stripe may throw at us.

Getting Started

If you don’t have a Rails app to work with, you can read their Getting Started guide here.

Since we’re using Rails, as always there’s a gem to help us out here. Stripe maintains stripe-ruby, a great library that should support all of the functionality that we’ll need. Let’s get it installed:

  1. Add stripe-ruby to your gemfile: gem "stripe"
  2. bundle install

Next, we made to make sure we’re actually including and initializing Stripe in our application. To do so, let’s add a new file at config/initializers/stripe.rb. Rails will execute this initializer after all frameworks and plugins are loaded.

require "stripe"

Stripe.api_key = ENV["STRIPE_PRIVATE_API_KEY"]

Pretty simple, right? To generate the STRIPE_PRIVATE_API_KEY, follow their documentation here. It’s generally good to store these kinds of keys in an environment variable, but it could also be hardcoded here for expediency. Just make sure not to check it into source control!

Creating our models

At the lowest level, the core Stripe resources we’ll be working with are a Product and Customers. In this walkthrough, we’ll be assuming we have just one product representing our subscription application.

First, let’s write the migration and create the corresponding models. In all of the migrations below, the purpose of the column is provided in the comments.

class AddBillingEntities < ActiveRecord::Migration[5.2]
  def change
    create_table :billing_products do |t|
      t.string :stripeid, null: false            # To map to the Product in Stripe
      t.string :stripe_product_name, null: false # The name of the product in Stripe


    create_table :billing_customers do |t|
      t.references :user

      t.string :stripeid, null: false # To map to the Customer in Stripe
      t.string :default_source        # Stripe identifier for the user's default payment source (initially null)

class BillingProduct < ApplicationRecord
  has_many :billing_plans # Ignore this for now, we'll be adding this later
class BillingCustomer < ApplicationRecord
  belongs_to :user, { required: false }
  has_many :billing_subscriptions # Ignore this for now, we'll be adding this later

To associate our product with a price and recurrence period, we’ll next create an entity to correspond to our Plan. We should also then ensure we’ve added the has_many association to BillingProduct that we commented on in the snippet above.

class AddBillingPlans < ActiveRecord::Migration[5.2]
  def change
    create_table :billing_plans do |t|
      t.belongs_to :billing_product, null: false              # The BillingProduct that the BillingPlan belongs to

      t.string :stripeid, null: false                         # To map to the Plan in Stripe
      t.string :stripe_plan_name                              # The name of the plan in Stripe
      t.decimal :amount, precision: 10, scale: 2, null: false # Price of the plan, in the corresponding currency's smallest unit (i.e., cents)

class BillingPlan < ApplicationRecord
  belongs_to :billing_product
  has_many :billing_subscriptions # Ignore this for now, we'll be adding this later

Lastly, we need a way to track our Subscriptions. These map a specific customer to a product via a plan. This is also when we should ensure we’ve added the has_many associations to BillingCustomer and BillingPlan that were commented on above.

class AddBillingSubscriptions < ActiveRecord::Migration[5.2]
  def change
    create_table :billing_subscriptions do |t|
      t.belongs_to :billing_plan, null: false     # The BillingPlan that the BillingSubscription belongs to
      t.belongs_to :billing_customer, null: false # The BillingCustomer that the BillingSubscription belongs to

      t.string :stripeid, null: false             # To map to the Subscription in Stripe
      t.string :status, null: false               # The status of the Stripe subscription (trialing, active, etc.)

      t.datetime :current_period_end              # When the current subscription period will lapse
      t.datetime :cancel_at                       # If set to cancel, when the cancellation will occur

class BillingSubscription < ApplicationRecord
  belongs_to :billing_plan
  belongs_to :billing_customer

Hooking up our core models

Now let’s write some services that will help us synchronize our entities with their counterparts on Stripe.

Generally, you won’t be creating or modifying Products or Plans very often. However, we still need to be able to synchronize them quickly and easily with Stripe to ensure we’re never out of sync and causing billing issues for our users. This includes accounting for the various updates that could happen: a Product or Plan can be created, updated, or deleted.

Let’s write a couple of classes to do just that. We’ve commented out the blocks of code for clarity.

class SynchronizeBillingProducts
  def call
    # First, we gather our existing products
    existing_products_by_stripeid = BillingProduct.all.each_with_object({}) do |product, acc|
      acc[product.stripeid] = product

    # We're also going to keep track of the products we confirm exist on Stripe's end
    confirmed_existing_stripeids = []

    # Fetch all of our active products from Stripe
    Stripe::Product.list({ active: true })["data"].each do |product|
      # If we are already aware of the product. let's just update the non-static fields on our end
      if existing_products_by_stripeid[product["id"]].present?
        existing_products_by_stripeid[product["id"]].update!({ stripe_product_name: product["name"] })
      # If we're not already aware of the product, let's create it on our end
          stripeid: product["id"],
          stripe_product_name: product["name"],

      confirmed_existing_stripeids << product["id"]

    # Lastly, delete any products on our end that no longer exist (or are not active) on Stripe
    BillingProduct.where.not({ stripeid: confirmed_existing_stripeids }).destroy_all

class SynchronizeBillingPlans
  def call
    # First, we gather our existing plans
    existing_plans_by_stripeid = BillingPlan.all.each_with_object({}) do |plan, acc|
      acc[plan.stripeid] = plan

    # We're also going to keep track of the plans we confirm exist on Stripe's end
    confirmed_existing_stripeids = []

    # Fetch all of our active plans from Stripe
    Stripe::Plan.list({ active: true })["data"]
      .each do |plan|
        # If we are already aware of the plan, let's just update the non-static fields on our end
        if existing_plans_by_stripeid[plan["id"]].present?
            stripe_plan_name: plan["nickname"],
            amount: plan["amount"],
        # If we're not already aware of the plan, let's create it on our end
            billing_product: BillingProduct.find_by({ stripeid: plan["product"] }),
            stripeid: plan["id"],
            stripe_plan_name: plan["nickname"],
            amount: plan["amount"],

        confirmed_existing_stripeids << plan["id"]

    # Lastly, delete any plans on our end that no longer exist (or are not active) on Stripe
    BillingPlan.where.not({ stripeid: confirmed_existing_stripeids }).destroy_all


These services can be invoked manually when you know you’ve made changes from the Stripe dashboard. We’ll also discuss hooking them up to webhooks below.

🛠 Before moving on, create a Product and Plan in Stripe and call your new services. They should now be in your database as BillingProducts and BillingPlans!

Subscribing a user

Now that we’re able to synchronize our core billing entities, we can get started actually subscribing a user to our product via one of the plans.

First, we need to be able to create a Customer for one of our users. To create a paid customer, this requires a Stripe token, most often generated client-side using one of their fantastic JavaScript libraries. A Customer can be created without a token, but will not be eligible for any paid products (only free or free trials).

We’re going to assume here that you have a User class with both email and name fields.

class CreateStripeBillingCustomer
  def call(user:, stripe_token: nil)
    # First, we fetch all users with the same email
    existing_customers_with_email = Stripe::Customer.list({ email: })["data"]

    # If we've found any matching customers for the user, grab it
    if existing_customers_with_email.size.positive?
      stripe_customer = existing_customers_with_email.first
    # Otherwise, let's create a new customer for the user
      stripe_customer = Stripe::Customer.create({
        source: stripe_token,

    # Lastly, let's make sure we persist the customer on our end
      user: user,
      default_source: stripe_customer.default_source,

Now that we’re able to create a Customer and associate it with one of our users, we’re ready to subscribe them to a plan.

🛠 Before moving on, try to subscribe a test user to one of your products (invoke a service manually if necessary). The subscription should now appear in your Stripe dashboard, and your user should have a BillingCustomer with a BillingSubscription!

Staying in sync using webhooks

The last thing we’re going to cover is handling webhooks from Stripe when anything occurs or changes related to any part of our billing system. This ensures we’ll always stay in sync and avoid any unknown billing bugs or issues.

First, you’ll need to activate webhooks and specify an endpoint in your Stripe dashboard. When doing so, you’ll be required to specify the types of events you want to be notified about. For the purposes of this walkthrough, these are:

  • plan.updated
  • plan.deleted
  • plan.created
  • product.updated
  • product.deleted
  • product.created
  • customer.updated
  • customer.deleted
  • customer.subscription.updated
  • customer.subscription.deleted
  • customer.subscription.created

Next, we’ll need to add a new route handler at the endpoint you provided to Stripe. Here we need to both verify the signature of the webhook request as well as handle the event itself.

class StripeWebhooksController
  def receive
    # Get the event payload and signature
    payload =
    sig_header = request.headers["Stripe-Signature"]

    # Attempt to verify the signature. If successful, we'll handle the event
      Stripe::Webhook::Signature.verify_header(payload, sig_header, ENV["STRIPE_WEBHOOK_SIGNING_KEY"])
    # If we fail to verify the signature, then something was wrong with the request
    rescue Stripe::SignatureVerificationError
      head 400

    # We've successfully processed the event without blowing up
    head 200

You’ll notice there’s a new environment variable here: STRIPE_WEBHOOK_SIGNING_KEY. This should be available in your Stripe dashboard for the webhooks endpoint you just created.

Lastly, we need to make sure we’re handling each of the individual events we’ve subscribed to. We’re not going to go into detail here on each of the handlers we’ve implemented, as you should now have the basis to move forward and write those yourselves.

The few exceptions are those related to Products and Plans: since we expect these to be updated so rarely, we can just re-invoke our overall synchronizer services that we wrote earlier!

class HandleStripeEvent
  def call(event:)
    case event["type"]
    when "product.created", "product.updated", "product.deleted"
    when "plan.created", "plan.updated", "plan.deleted"
    when "customer.updated"{ event: event })
    when "customer.deleted"{ event: event })
    when "customer.subscription.updated"{ event: event })
    when "customer.subscription.deleted"{ event: event })
    when "customer.subscription.created"{ event: event })
      raise("Unexpected event from Stripe: #{event['type']}")

🛠 Now that we’re handling these various events from Stripe, you should now be able to update any of your entities (Product, Plan, Customer, Subscription) in the Stripe dashboard and see the changes reflected on your end.

Wrapping up

This walkthrough should get you set up with a simple subscription billing system so that you can start charging for your product.

In a later blog post, we’ll talk about doing something a little more complex: incentivizing in-app actions (such as referrals) with coupons on the user’s billing plan.

❗️ Are you a software engineer?

At Monolist, we're building software to help engineers be their most productive. If you want to try it for free, just click here.

Follow us on Twitter