When our engineering team is asked how we are able to develop products so quickly and with such high quality, the first answer is always the people. Our engineers, from AI researchers and fintech veterans to hackathon winners and college dropouts, are all world-class engineers, and their creative relentlessness is what drives our product forward every day.
But the second answer is the culture - the ways that these people build systems, make decisions, and work together. As Ramp grows, we’ve attempted to codify this culture by writing down a few of our engineering principles. Of course, these are not ironclad rules and we fully expect them to change over time. But they represent ideas that have served us well thus far and which set us apart from many other engineering cultures we’ve seen. Here are 7 engineering principles that we believe have contributed to Ramp’s incredible success over the past 2 years:
Every system we build at Ramp has its own unique requirements and context that we base our technical decisions on. How bad is it if the system messes up? How much business value will be created by moving quickly? Is this system going to be a long-term solution or a short-term fix? Does it run in the background or will the customer be sitting and waiting for it to finish?
What does this mean in practice? It means not insisting on consistency across codebases (stylistic consistency, data model consistency, and even language consistency) when there are other considerations at play. Each engineering team is free to use coding practices that match the problem they’re solving, rather than having to negotiate a company-wide consensus when trying new things (though consistency is still a great default, of course).
As a new engineer on a system, try to learn why it was designed the way it was and be prepared to operate under assumptions which are different from the ones governing the last system you worked on.
When building products and features at Ramp, we often first launch an "MVP" which has less scope than the full-fledged spec that we plan on tackling later.
Inevitably the question arises - should I build a quick version that supports what we're doing now, or should I build the more powerful version that will support what we'll be doing in 6 months?
And the answer is: Try to build a quick version that both supports what we’re doing now but also is flexible enough that it won’t slow us down when we come back to it in 6 months.
Reversibility is all about how hard it is to make changes in the future. Some decisions, like a customer-facing API interface, aren’t easy to change later on, and so you should take the time to do it right. But if you optimize prematurely, build everything to last forever, and are cautious about every decision, you will be too slow. At Ramp, we value speed, and strive to optimize for the simple now and the complex later. Always ask: is there a version of this that is quick but also extensible? If you can find one, you’ll be able to move fast now and also move fast later - the best of both worlds.
In a well-designed system, the tables correspond to the objects or records that the customer is manipulating, and the functions correspond to the buttons that the customer is clicking. Every time you have to rearrange your data in order to make it understandable to the user, you're adding new points of failure and new layers of translation. While this is necessary in some cases (you should pick the right approach for the problem at hand), it should be avoided when possible.
Collaboration with product and design is absolutely critical before you start coding. You don't need to know exactly what color all the buttons will be, of course, but you do need to know how the user is thinking about your system’s job. When you design a system before the core elements of the spec are finalized, you usually either build the wrong thing or you build it the wrong way. Sometimes one use case can elucidate the crux of the matter, and many tech and product decisions follow from it. Other times, an object's meaning can change in the product discovery phase, and it will acquire capabilities that you would not have imagined or prepared for.
A nice side-benefit of this principle is that you'll be able to meaningfully contribute to the product/design discussions. Take your insights about the system and translate them into insights about the user - when the two are thinking along the same lines, you'll find that you can see ideas and patterns that no one else can see.
If you were to sit in on a product, engineering, or design discussion at Ramp, you’ll frequently hear that we want to be “opinionated” about how we do things. Much like there is a “Pythonic” way of writing Python, there is a Ramp way of issuing and managing expenses.
We are building the best product on the market - and that means making clean, simple experiences. Think about an Apple TV remote versus a no-name remote - which has more buttons? Instead of adding endless menus and toggles like Expensify or Concur, we build the experience the way it’s meant to be. We don’t build features that lead to clunky user experiences, even if that’s what the customer is asking for.
How does this impact engineering? Too often, engineers feel like they must choose between building an infinitely flexible system, with all the complexity and careful design that it requires, or else resign themselves to years of ad-hoc fixes to the issues caused by limitations in the original design. Instead, with a strong view on how the product will and will not evolve in the future, we can build simply and with speed, while simultaneously avoiding costly rebuilds.
Whenever two things resemble each other, there’s a temptation to have them share code. But if you don't have the right abstraction, this will be very costly. This piece spells it out nicely.
Here are two examples that illustrate this principle.
Example One:
The three variants of the F-35 fighter jet all have slightly different use cases and, as a result, “the three variants share just 25% of their parts, far below the anticipated commonality of 70%.”
Example Two: When Microsoft was developing Word for Mac, they initially decided to have it share the same codebase as Word for Windows. This was meant to save development time and also improve the product by ensuring that Word’s features were consistent across platforms.
They shipped a version of Word for Mac based on this idea, and it was a complete disaster: Mac users hated it, to the extent that the company got hate mail about the product.
Luckily, on the second try, they took a new approach and wrote different software for Mac which took advantage of what the Mac could do and optimized for things that Mac users cared about. It became a multi-billion dollar win for both Apple and Microsoft.
However - it is also important to keep in mind: The right abstractions pay dividends for years. They tell stories and help the brains of people working with them and around them, and they shape the culture and the products to come. They are much more than just finding commonality.
Ramp constantly faces these sorts of questions as we build out many different spend products (cards, reimbursements, bill payments). These products have nuances which are critical to the UX and critical to the code as well. For example, on cards the money moves when the transaction is created, while on Bill Pay the money moves only when the bill is approved. So, for today, we live with the duplication because we haven’t found the right abstractions. But we are always seeking them out.
Ramp operates in a field where our customers have grown accustomed to slow, manual processes and legacy technology, and it is our job to show them a better way. This means that when they have complaints or suggestions, we always investigate deeply before building anything.
For example, one of our customers said they required the CFO to approve all spend above a certain amount. When we asked them, “has he ever actually rejected spending that was approved by his team?” The answer was, “no, but he wants to stay in the loop.” When we set up Ramp for this customer, the CFO is merely notified of all large expenditures - so that he is still in the loop, but is no longer a bottleneck.
As the makers of Slack once wrote, “The only real, direct measure of innovation is change in human behaviour… no small innovation ever caused a large shift in how people spend their time and no large one has ever failed to do so.” Ramp is in the business of changing how companies manage their finances. So while we rely on our customers to tell us their biggest pain points, we always build the solution we believe in - even if it’s not the one they asked for.
Fundamentally, the reason we ship code is in order to solve customers’ problems. And most problems, it turns out, are rather multi-dimensional. Thus, every project in its initial stage is a bundle of ideas and proposals, each addressing a different piece of the problem at hand.
At Ramp, when we make tech specs and product specs, the first thing we do is to take those ideas and break them down into their smallest possible components. Then each component can be prioritized according to the incremental value it adds on top of the previous ones, as well as the incremental effort required to build it. And most of the time, we find that we can achieve 80% of the value of the initial idea with just 20% of the effort.
Another benefit of this decomposition technique is that each small chunk can be owned by a small team. Small teams can move faster than large ones, since they can coordinate amongst themselves more easily and share context. At Ramp, each portion of our product is owned by one of these teams (we call them “pods”) so that the people on that pod can accumulate deep knowledge of the code, the customer needs, and the roadmap.
Want to find out what the pods are currently working on and potentially join one? Apply for a job here: https://boards.greenhouse.io/ramp.