How Superside built an automated credit system from scratch and grew subscription with 340%
Inspired by ledgers and mobile data subscriptions, we launched an hour-based credit system together with new subscription plans last year. That lead to hyper-growth. We will be looking into how to build such a system, and we used it to automate invoicing
What is a ledger?
A ledger is a “book” of all the transactions a business has completed. In account, it is splitter up into multiple accounts or books, for example, cash, expense, assets. Each ledger entry is append-only and immutable (cannot be changed or deleted).
In blockchains, the “book” is a distributed ledger consisting of multiple nodes. The transaction gets written in the book when a majority of nodes verify the transaction through a majority vote. You can only add transactions to the book, but not deleted. The idea is that you can always trust that the balance is correct, as the balance is never stored anywhere, but calculated through that the wallet scans through the ledger and aggregate the number. You also gain full transparency over the transactions. By never storing the value, but deriving it, your data will always be correct. Reports also won’t need to be pre-calculated but can be done on the spot.
Our credit system
Superside is a subscription service that delivers full-stack design to enterprises. In each of the plans, you are included x amounts of work hours of design work of a certain type (essentials, full-stack, campaign) that have different rates. Parts of the x hours also include fast turn-around with an additional fee for 24-hour turnaround and 12-hour turnaround. Inspired by mobile data providers, we also wanted to have automatic data rollover.
Our credit system started out as homegrown excel-sheets, that needed manual bookkeeping, but we quickly saw that we were struggling to track back the transactions (what project used this amount?). It was also hard to figure out how many hours will be rolled over, what kind of design hours, the full transaction history, and a guarantee of the correctness.
We also wanted to always provide a clear answer on how many hours the customers had left on their plan, and reports on how they spent their hours during each month. Most billing subscription tools were made for SaaS, where you either pay per resource (e.g. $10/user/month or $5/license/month), or charge a dollar amount per unit (e.g. $0.1/gb/month). In our case, the existing saas offerings weren’t sufficient, as we had multiple types of design, delivery time, rollover, and full transparency of which projects we were using credits, and which we didn’t. Builder a ledger seems to be the way for us.
Building a ledger
Before starting to code and looking at the data structure, we started to define the properties of the subscription system:
- Every month, we add x hours of a design type where y hours is fast turnaround.
- Fast turnaround hours get converted to non-fast-turnaround after one month.
- Credits expire after three months.
- Possible to perform manual operations, like adding hours.
- Optimization: If it was log projects with a design type of a lower price, we could use the credits to cover this, if we had hours left unspent. But if we at the end of the month see that we have used our credits un-optimal, we should reallocate the credits and cover the most expensive projects.
- multiple reports to give everyone full transparency of how much credit is used, and what they are used for
We then started with some observations:
- There are actually two types of operations we need to support: Operations on the credits, and operations on hours. To keep it simple, we will define them in two different ledgers.
- Instead of making it multidimensional, with one column for each designated design type, and one for fast-turnaround, we could use a single list: Essentials, Essentials_Fast_turnaround, Full_stack, Full_stack_fast_turnaround, Campaign.
- For fast-turnaround hours, we could have a separate column for the date, telling the expiration date.
- We want to mark every operation with a changeId, to be able to group operations that have multiple entries together.
- Operations with timeEntry=null.
- By setting effectiveDate and expireDate, we can manage credit rollover.
We were now ready to build the structure:
enum class DesignType{
Essential,
Essential_Fast_turnaround,
Full_stack,
Full_stack_Fast_turnaround,
Campaign
}enum class Reason{
MONTHLY_ADDITION,
ADDING_CREDITS,
LOGGED_TIME,
NEGATED_TIME,
OTHER
}enum HoursUsedLedgerItem{
val id: UUID,
val timeEntryId: Long,
val designType: DesignType,
val changeId: UUID,
val reason: Reason,
val amount: BigDecimal,
val effectiveDate: LocalDateTime? = LocalDate.now(),
}enum CompanyCreditLedgerItem{
val id: UUID,
val companyCreditId: Long,
val timeEntryId: Long,
val designType: DesignType,
val changeId: UUID,
val reason: Reason,
val amount: BigDecimal,
val effectiveDate: LocalDateTime? = LocalDate.now(),
val convertingToNonFT: LocalDateTime,
val expireDate: LocalDateTime?
}
We are now ready to define some operations:
- Adding credits:
Adding hours is the easiest, and would be to add a new CompanyCreditLedgerItem. Note that in the case where you have x hours where y is fast turnaround — you would add x-y hours non-hours and y fast-turnaround hours. - Removing credits
Removing is equally simple by appending a CompanyCreditLedgerItem with companyCreditId = originalId and expireDate =originalexpireDate and a negative amount - Calculate hours of credits left this month
Start by fetching all the entries of CompanyCreditLedgerItem with effectiveDate≤ startOfmonth and (expireDate ≤ endOfmonth), and sum the amount together. We can also group it by companyCreditId to get it grouped by the different credits. - Logging hours
Every time someone logs hours, we will listen to the event and create an entry in the HoursUsedLedgerItem. We would also use the list created in #3, to figure out where we can withdraw hours. We will then add entries with a negative amount in CompanyCreditLedgerItem to remove credits from there. - Optimizing credits used (the hardest part)
What happens when you log hours with no credit left? In this case, we should consider if we should rearrange the credits used. Maybe are you using credit with “concept” hours to cover a project with “production” hours.
The trick here is to recognize that this problem has a lot in common with the insertion sort algorithm.
A. Sort all credits spent this month by the project rate that we can use to cover this time entry, with a credit type that can be used to cover logging this entry.
B. Find the entries, where the project rate is lower than the rate of the time-entry logged from the list of A
C. If there is a case that we find a credit entry that has a lower project rate than the rate of the time entry logged, we should rather use that credit to cover this rate.
D. We “insert” our current timeEntry in front of the criteria in B, which means to cover this credit, and shift the credits covered. We don’t need to look at credits outside of this range, as it is already sorted. This can be done by adding CompanyCreditLedgerItem with a negative amount and running the “logging hours” procedure described in step 4. - Find overage hours (hours over the plan) in a month
To find overage hours, we take all the entries in the ledger this month, and all the entries in the companyCreditChange with timeEntry!=null and effectiveDate this month. We can now group these two lists by timeEntry, and sum together the hours. The non-null entries would be those who aren’t covered with a credit.
We now have a real-time report of the monthly cost. We can even create a replay functionality for the whole month, by adjusting the time range.
Automating invoicing
We have now all the essential building blocks to automate the invoicing.
- We know what subscription plan each team is on. Add that monthly amount to the invoice.
- We found overage hours for a month in the last section. Add that to the invoice.
- Autogenerate an invoice at the start of each month adding #1 and #2.
The results
The result is an automated system with real-time reporting and guaranteed correctness — all by building a ledger.
We are currently in hyper-growth and are looking for great engineers to join us. Ever wonder why scaleups have the best talents? Find out more here