The story behind Wave's boring Monetization release
How did monetize our 10+ year product without disrupting customers?
Introduction
On January 29th this year, a seemingly grandiose thing happened. We started monetizing some core features in our 10+ year old product. Beginning then we invited new customers to purchase a subscription to enjoy some exclusive features in our product that existing users enjoyed for free. The launch itself was overwhelmingly uneventful, and admittedly, somewhat anticlimatical. It was however, successful. This success was due in large part to the fact that, we didn't do anything big on the day of the launch. The heavy lifting was done months before... And that's what this story captures.
So we wanted to add a price tag to some features in our product... How hard could that be? Couldn't we just get a marker and someone with great penmanship and get it over with? Hmmm... lemme take that one to the project retrospective. In the mean time! Take a product and a company that's both over 10 years old and you've got an entire ecosystem that likely isn't ready for your new grand vision. The passage of time shapes and skews your system with a particular slant. The system is a wonderful mix of customer features, best practices, mix in some anti-patterns and technical debt. All of it contributes in some way to friction that makes fundamental, cross-cutting changes much more interesting... where "interesting" could be anywhere from exciting, thrilling, to overwhelming, harrowing or near impossible.
In our case, our age forced us to carefully work through several technical and non-technical challenges to make our dream a reality. Many departments outside Engineering became key stakeholders. Our customers would need extra support from our Customer Experience team as we transitioned. The team also needed to understand and identify paying customers in the systems they used. One of our adjunct departments that provided coaching services for customers would be impacted by our regulation of premium features. Conversations and solutions would be necessary to prevent disruption for coaching clients. The list of departments, dependencies and interconnections grew as the project aged.
Inside Engineering, almost all our teams had a role to play in the project. Our product teams needed to make changes to incorporate and enforce the distinction between paid and free features. Our frontend and mobile teams would work on the the experience that allowed customers to purchase a subscription and gain access to premium features. Our API team needed to enable the regulation of access to premium features. And well, we needed a net-new system to regulate access to these premium features -- that's where our team came in! Also, also... our Data team would need information from our system to provide reports for our Marketing team and other areas of the business.
For our grandiose monetization project, we introduced a net-new service that regulated access to premium features. The introduction of this service created a new dependency that sat at the centre of all our products and many departments too. We worked with multiple departments, micro-services, technical debt, scope reductions to realize our monetization vision. It was no easy feat, but it's a beautiful story to reflect on.
Everything before the launch
Well... that was one long introduction! Let's transition to talking about what we did before the launch, and the steps that were critical to the success of the launch. First, I'll describe the new microservice we built and the use cases it was intended to service. Next, I'll walk through the timeline of the project, highlighting the strategies we took that helped us de-risk the launch.
The newest service around town -- Are you entitled?
Our team was tasked with building a new microservice that was responsible for regulating access for premium features and facilitating the purchase of subscriptions that provided access to said features. We called the service the "Entitlements" service because of the dominant use case of determining whether a business is "entitled" to a certain feature. Our users could purchase a plan (which provides a set of features) for one or more of their businesses in Wave. Before purchasing a plan, users would be on a plan (STARTER plan) which gave access to some features. Purchasing a PRO subscription would give them access to more features.
Our system was responsible for answering questions like...
Does this business have access to this feature?
What plans can I purchase for my business?
What plan(s) do I currently have for my business?
Which subscriptions have I purchased for my business?
And our system would also facilitate, or perform the following tasks:
Put newly created businesses on a plan
Enable customers to purchase, upgrade or cancel a plan
So, with our new microservice we could get context on access businesses had to premium features and plans could be purchased.
The Entitlments service would also introduce some new interconnectedness into our existing microservices. Our product teams would rely on our service to provide sufficient context to present the appropriate experience for customers. Our invoicing product for example would need to show customers on our free (STARTER) plan nudges to access premium customer reminders. Our User Management feature which allowed our business owners to add other users to their business would need to represent some user types as premium ones. Our frontend and mobile teams would need to leverage our information on plans to show customers their current plan and upsell our premium plans. Our public API which proxies all traffic through to our backend microservices would need to provide capabilities for us to enforce the regulations of the Entitlements service. The Entitlements service represented a new core dependency for almost all our existing microservices.
What did we do?
Now let's traverse the timeline and call out some strategies that were crucial to the success of the project. It's mostly chronological, but there might be some overlap...
Consumer-driven design
Our first step was to meet with each of the teams (there were about eight of them) to understand their requirements so we could design, plan and prioritize. Each product team had a mandate crafted by their respective product leads for the changes they needed to make. This mandate would shape their interaction with the Entitlements service. Understandably, no product was identical but there were lots of commonalities. This information was critical for planning and determining where we place our focus.
In meeting with the teams we sought to understand interactions with our service, shared our initial API designs, then captured the dependencies they would use. In our initial meetings, we aimed to capture all interactions with our systems by framing questions around the subscription lifecycle along with other general questions. We asked questions like:
What actions does your service need to take when a business purchases, activates, cancels or resumes a subscription? This helped us understand what data teams needed to pull out of our system via an API and also data we needed to push to them (e.g. via event streaming using Kafka)
What information do you need to support your use cases in those specific scenarios? This helped us tease out the details of our APIs.
Where are your features implemented (e.g. in a backend service, in our single page app), and what libraries are being used (e.g. React, Redux). These questions helped us understood where abstractions were most needed. While our single page app is in React, there were important differences across the teams. Some teams were using different libraries for state management (e.g. Redux, local component state using GraphQL) and different component styles (e.g. older class-based vs functional components).
Are there legal, regulatory or compliance requirements that we need to consider? This helped us understand any constraints we'd need to bake into how our system worked. Our Payments team for example, ended up doing some extra checks to ensure there was no de-sync between our systems. They needed to ensure that the access our system described matched the access they were implementing on their side. This had monetary implications for customers.
Are there any risks or roadblocks you forsee that we'll need to mitigate? For example, are there any endpoints that are really high in traffic, or require low latency? Through these questions we uncovered a use case where our Accounting team needed to submit bulk, bursty traffic over small periods of time.
Once we had documented the responses for all of these questions and met with the teams, we could transition to designing capabilities. We separated the system into logical chunks and created tickets for engineers on the team to pick up. The team discussions provided a rich set of context that we transferred to the tickets, and could be used as acceptance criteria. This simplified the design process for our backend APIs and our frontend abstractions. We had a clear set of use cases we could reference. When these designs were completed, we met with the teams to show them our designs.
Meeting with the teams helped us determine the full set of capabilities we needed to build to satisfy all the use cases for the teams. From these conversations, we knew how many teams needed which capabilities, and why. Presenting these capabilities to the teams helped us validate the fulfilment of their requirements.
Fake it until you make it -- unlocking with contracts
After our APIs and frontend abstractions were designed, we opted to build fake capabilities to unlock the teams and save time. Recall, there were about eight teams that were dependent on our capabilities. Eight different teams meant lots of teams to work with and confirm that things actually worked when we implemented them. Additionally, while most of the teams would use a core set of capabilities, some teams needed some more nuanced capabilities to satisfy their requirements. It just wasn't practical for us to let all the teams wait until we had built both the common capabilities and more nuanced ones. It would take too long!
This is where things got interesting! We were able to go to production with these fakes months before the actual launch. We leveraged one of our product requirements that made this possible -- our existing users would continue to have the same experience after our launch. Our Product team decided that we would consider all our pre-launch users as "legacy" users, and they would retain access to the monetized features for free. When we launched, we would draw a line in the sand and all new customers would have to pay for the features we now deemed premium. However, nothing would change for pre-launch users. They would continue to enjoy Wave as they did before the launch.
Ok... big deal! So we decided to allow our existing customers to avoid paying for the premium features. What did that mean for our capabilities. That meant we had until the launch to fake the "legacy" experience. All our capabilities could statically treat all users as though they were legacy users. So, we could fake our entire backend APIs on the frontend by faking the legacy experience. In more concrete terms:
Our API that answered the question "is the business entitled to X feature" would always return True.
The API that provided the list of features the business has access to would always return the full list of features.
Our API that returned informations on plans one could purchase would always return a static set that are available to our legacy users.
So faked it we did! All of our frontend abstractions and backend APIs initially returned a canned list of responses. The frontend abstractions didn't need to hit our backend APIs and our backend APIs never needed a database or any other persistence mechanism. Fake, fake, fake! We built React hooks and higher order components that returned fake responses. Some teams needed to hit our backend APIs. For those, we built the services and repository layers, but they all fetched data from constants. We also provided some knobs that allowed teams to configure our fakes. This allowed teams to test different scenarios manually, or using automated tests.
Building the fakes provided a trove of benefits! For starters, it allowed all the other teams to start working in parallel while our team started building out the real implementations. It meant we could start to get feedback early about bugs, and interaction issues with our service -- lots of early validation. It also saved time! The teams were mostly able to finish off their implementation before we were through with ours. It also provided really high confidence that the use cases worked! Teams were able to go to production once they had completed their use cases! Faking our capabilities using contracts really helped optimize the timeline and provided great feedback for the entire implementation.
Enough faking -- the launches before the launch
The real launches took place as we gradually removed the fakes we had in production. Internally we crafted a detailed rollout plan, that specified how and when we would remove the fake capabilities on the frontend and the backend. It was something of a masterpiece! We started with providing access for premium features, then enabled the purchase flow, which ultimately prepared us for the external launches to Canada and the US. Additionally, we did some technical design that reduced the likelihood of regressions and mistakes during surgical removal of fakes.
Our rollout plan outlined that we would prioritize access information for features, then move on to the purchase flow. This meant all our products could implement the distinctions in functionality for premium flows. Our Invoicing products, for instance, could show sending automated reminders as premium for businesses without access to the premium feature. These calls would now go straight to our backend, through our frontend abstractions. Additionally, teams that hit our backends directly would benefit from responses that were backed by persistence and real logic. Focusing on the purchase flow enabled us to simulate paying customers. We had APIs that provide information on what plans a customer could purchase, and others actions like cancellation.
We also took extra caution to ensure we didn't mess up the fake removal surgeries. On the backend, we implemented a proxy repository pattern that enabled a clean separation of the fake from the real implementation. All calls to our persistence were proxied through this component which directed calls to our fake or real implementation. We took a similar approach on the frontend with some differences. The goal was to ensure that it was crystal clear, what was being faked and what was not. Largely, this extra work paid off as we were able to avoid regressions and didn't have major bugs or incidents while we swapped out the fake implementations.
All in all, we did several mini-launches which greatly de-risked the entire initiative. Each fake we removed meant we were closer to the application behaviour we needed for our external launch. All our capabilities were built and functioning in production, and our product teams were actively using those capabilities. Having our capabilities being actively used provided a high confidence that they worked as they should. It also allowed us to resolve some minor issues that came up. These launches represented the proverbial tree that fell in the forest.... No one knew!
So the actual launch then?
Fast forward to our launch date, on Jan 29 -- what did we actually do? Well... all we did was to change a configuration for how we treated users that signed up and the businesses they created. Prior to our launch date, all businesses created would be considered legacy. This meant users could enjoy our premium features for free. We would assign the necessary access (entitlements) to enable the business to access all our features. Being on a legacy plan disqualified users from purchasing our premium plans since they had all the features of the premium plan.
When we launched, we stopped doing that. After the launch, we flipped a switch and users and their businesses would now get free access to a smaller set of features. Additionally, they would now be eligible to purchase our premium plans. This eligibility enabled nudges sprinkled in each product to upsell our premium plans. And ahh... that's it! That's all we did! After we launch, we effectively stopped giving the legacy experience away, but instead provided less access. The heavy lifting was done months before when we removed our fakes and started using the system in production!
Conclusion
The journey of monetizing some core features of our decade-old product is a beautiful success story of continuous delivery. The essence of our success wasn't attributed to the steps on the launch day but rather the methodical, behind-the-scenes orchestration that allowed us to seamlessly transition to a monetized model without disrupting the user experience. The real work went in long before the curtains were raised. As we move forward, the lessons learned from this initiative will undoubtedly shape our approach to future projects. The successful launch of our monetization strategy serves not just as a milestone but as a beacon, guiding us towards continued innovation, customer engagement, and operational excellence.
I hope this narrative inspires you as embark on similar journeys!