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:
- Add stripe-ruby to your gemfile:
gem "stripe"
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
t.timestamps
end
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)
t.timestamps
end
end
class BillingProduct < ApplicationRecord
has_many :billing_plans # Ignore this for now, we'll be adding this later
end
class BillingCustomer < ApplicationRecord
belongs_to :user, { required: false }
has_many :billing_subscriptions # Ignore this for now, we'll be adding this later
end
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)
t.timestamps
end
end
class BillingPlan < ApplicationRecord
belongs_to :billing_product
has_many :billing_subscriptions # Ignore this for now, we'll be adding this later
end
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
t.timestamps
end
end
class BillingSubscription < ApplicationRecord
belongs_to :billing_plan
belongs_to :billing_customer
end
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
end
# 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
else
BillingProduct.create!({
stripeid: product["id"],
stripe_product_name: product["name"],
})
end
confirmed_existing_stripeids << product["id"]
end
# 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
nil
end
end
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
end
# 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?
existing_plans_by_stripeid[plan["id"]].update!({
stripe_plan_name: plan["nickname"],
amount: plan["amount"],
})
# If we're not already aware of the plan, let's create it on our end
else
BillingPlan.create!({
billing_product: BillingProduct.find_by({ stripeid: plan["product"] }),
stripeid: plan["id"],
stripe_plan_name: plan["nickname"],
amount: plan["amount"],
})
end
confirmed_existing_stripeids << plan["id"]
end
# 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
nil
end
end
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 BillingProduct
s and BillingPlan
s!
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: user.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
else
stripe_customer = Stripe::Customer.create({
name: user.name,
email: user.email,
source: stripe_token,
})
end
# Lastly, let's make sure we persist the customer on our end
BillingCustomer.create!({
user: user,
stripeid: stripe_customer.id,
default_source: stripe_customer.default_source,
})
end
end
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 = request.body.read
sig_header = request.headers["Stripe-Signature"]
# Attempt to verify the signature. If successful, we'll handle the event
begin
Stripe::Webhook::Signature.verify_header(payload, sig_header, ENV["STRIPE_WEBHOOK_SIGNING_KEY"])
HandleStripeEvent.new.call(payload)
# If we fail to verify the signature, then something was wrong with the request
rescue Stripe::SignatureVerificationError
head 400
return
end
# We've successfully processed the event without blowing up
head 200
end
end
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"
SynchronizeBillingProducts.new.call
when "plan.created", "plan.updated", "plan.deleted"
SynchronizeBillingPlans.new.call
when "customer.updated"
HandleStripeCustomerUpdatedEvent.new.call({ event: event })
when "customer.deleted"
HandleStripeCustomerDeletedEvent.new.call({ event: event })
when "customer.subscription.updated"
HandleStripeSubscriptionUpdatedEvent.new.call({ event: event })
when "customer.subscription.deleted"
HandleStripeSubscriptionDeletedEvent.new.call({ event: event })
when "customer.subscription.created"
HandleStripeSubscriptionCreatedEvent.new.call({ event: event })
else
raise("Unexpected event from Stripe: #{event['type']}")
end
end
end
🛠 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.
Top comments (4)
Hi,
Great article... It would be helpful for me if you can create an example app out of this tutorial..
Thanks
I am see a website that are working on Medical Billing Services in New York I want to make some code will you provide me it.
If you are looking the latest solution of Stripe subscription integration in ASP.NET, check out the demo >>> techtolia.com/StripeSubscriptions/
Hi,
It is possible to access the source code?
Thank you.