In April of 2021, I wrote the first lines of code for Ramp’s Bill Pay product. We publicly launched Bill Pay in October and it eventually became a multibillion annual total payment volume product — it continues to be one of the fastest growing products in the Ramp ecosystem. In my mind, there are two distinct sets of lessons I learned working on Bill Pay: 0 to 1 lessons and 1 to N lessons.
When building the first version of the system, we made a million decisions a day. There was so much surface area to cover and uncertainty regarding the exact product we were going to deliver. To some extent, we were forced to make decisions quickly. We needed to separate minor decisions from those that could long-term hinder our ability to ship quickly.
The biggest way we could have messed up in the early days of the project was by picking the wrong abstractions. These abstractions are the fundamental assumptions of the system we were building, and making the wrong assumptions would have made our code brittle and difficult to adapt to new requirements. Finding product-market fit (PMF) with a new product is all about iteration. If each iteration required breaking a core abstraction, we would not have been able to ship fast enough to find PMF.
A good example of one abstraction within Bill Pay was canonicalized payment statuses. Our payments system distills all payment states down to five possible statuses, despite the nuances of different payment methods. This allowed for higher product velocity on Bill Pay because we did not have to consider the complexities of payment rails while adding features.
To get a project of Bill Pay’s scope done, we needed to work effectively with other engineers. This involved breaking up the project into different parallel work streams and then combining those workstreams to create the final product. At first, I communicated by explaining in English a service we needed and hoped the engineer understood precisely enough to meet our requirements. However, I later learned a more effective approach was to implement a fake mocked function, send the engineer a GitHub link, and instruct them to make that function work. The beauty of code is that there's no ambiguity. By providing them with the mocked function, I communicated precisely what we needed. Unlike code, the English language allows for ambiguity and confusion.
The human ego often tricks us into thinking we know the perfect product that can solve all the users' problems. We start believing we're geniuses in our ivory towers who know exactly what customers need. Let's face it: we're not that smart. Until we actually put something in the hands of customers, we're pretty clueless. So, the best way to improve the product was to build something quickly, get some beta customers onboard, and talk to them. They told us how we messed up and how we could make the product better.
Side note: customers won’t always tell you the parts of the product that suck explicitly so it’s better to use session replay tools like logrocket and see where they get stuck or their mouse shakes around in a frustrated way.
It’s canonical marathon runner knowledge that 13.1 miles is not the actual halfway point: half the mental effort occurs after mile 20. Thus, in your head you should treat mile 20 as the actual halfway marker. The same is true when building products: you build in the dark for months, make best guesses about what customers want, piece together different customer interviews and competitor breakdowns until and you have an end to end product. You show it to customers, you feel like you’re almost done. You’re not. The first customer you show a product to is always gonna hate it. Great products are not born great products, they require quick iteration and mental resilience to keep putting in the hours even after customers tell you your product sucks. This is mentally the most difficult part of bringing something new to the world. I would reference this quote whenever I was feeling down in this time period:
“Look at a stone cutter hammering away at his rock, perhaps a hundred times without as much as a crack showing in it. Yet at the hundred-and-first blow it will split in two, and I know it was not the last blow that did it, but all that had gone before.” - Jacob Riis
Have faith the stone will split, continuously take feedback and integrate it — eventually you’ll have a product people love.
If you did the 0 to 1 part right, there should be a fair amount of duct tape holding your product together (hopefully nowhere super important but there should be some). The initial scaling of the product is where you learn what duct tape you need to replace ASAP and what aspects you can keep on life support a little longer. In the early days, we did a lot of manual tasks that just wouldn't fly when we started seeing huge spikes in usage. This was the phase when we had to convert some of those hastily implemented features into robust and scalable systems.
It's essential to maintain a delicate balance because you can't scale everything at once. You have to prioritize. It's usually not feasible to transition every manual process and ad hoc solution into a fully automated, scalable system overnight. We had to carefully pick which parts to focus on first, considering factors such as the impact on user experience, potential for error, and the volume of manual work required.
As our user base grew, so did the stakes. Any issue that would have affected a handful of customers in the early days now had the potential to disrupt thousands of users. We had to implement more comprehensive testing and monitoring systems, as well as robust contingency plans for when things inevitably went wrong.
When you have no users, you’re in pure value generation mode. YOLO merges have to be your bread and butter, because all that matters is how quickly you can ship. You have to give up that mentality when people are trusting you with their AP process. Gradual rollouts, comprehensive QA processes, and extensive monitoring become key in maintaining customer trust.
If you’re writing anything down before you have any users you’re wasting time, but once it’s time to add more engineers to a project taking the time to write documentation is one of the highest leverage things you can do. Giving resources to new engineers cuts down time to productivity by a lot, and overall makes the onboarding experience better for everyone involved.
Documentation is a product in and of itself, so take a first pass, use it for onboarding and get feedback on which parts weren’t clear and adapt. Eventually engineers should be able to completely self service onboard onto the codebase.