Welcome to Money In Transit, the newsletter for startup founders who find themselves dragged into payments technology. I’m Alvaro Duran.
Today’s post is inspired by Nick Janetakis’s What I Learned about Payment Systems While Working at a Pizza Place, and it’s an exploration on the requirements for any useful payment application. Nick’s post is great, but I felt that it failed to capture completely all the necessary details that every payment application must have.
Even though it goes into some technical details, there is no code in this post. Both engineers and product owners alike should benefit from it. Engineers will gain enough domain knowledge to start up, and product owners will gain enough technical insight to improve some of their tech team’s initiatives.
Consider sharing this post with someone who has recently complained about the state of their payment application.
It is very frustrating to see how often payment applications cause a lot of harm and lose a lot of money for the companies that build them.
Payments is one of those business areas that seem easy, but are deceptively complex when you look into them closer. Because of that, it’s common for developers and product owners to make false assumptions when they build payment applications. This results in products that become an obstacle, rather than help, for the clerks and workers that use them.
Do you want to build useful payment applications? Then, you have to understand, completely, how they’re used in the wild.
In this post, I’m going to examine how a pizza place handles the payment experience, and I’m going to extract a few lessons that I hope will be valuable for developers and product owners working on money software.
If that sounds interesting, then keep reading.
This is what ordinarily happens hundreds of times at your favorite pizza place during most weekends:
The Customer places an Order
Each Item is listed with a name, quantity, note and price
The Customer gets asked for their name/phone number/email
The total for the Order is calculated including things like taxes, delivery fees, tips and discounts as individual Items
The Order gets created
An Invoice gets created as a consequence
It will get paid now (the order came through DoorDash/UberEats, or through the pizza place’s website), in a few seconds (at the counter) or later (at pickup)
Two independent things must happen at this point: the Order needs to get fulfilled, and the Customer needs to pay for it.
When the Customer pays for the Order, a Receipt will get created
Even though this is just the happy path, there’s already a ton going on. I will unpack this and develop a model as a result.
Decoupling Orders from Payments
The most important thing to notice is that placing an Order (the what) and processing a Payment (the how) are two separate issues.
It would be a mistake to simply create a massive table in your database with the combination of the two. Instead, they must be kept separate.
Why? First, because their life cycles are totally independent: you can have an Order paid before completion (the Customer placed the order via DoorDash/UberEats), while it’s been fulfilled (what McDonald’s does when you buy via one of their kiosks) and sometimes even paid after completion (when you pick it up).
Second, though, because the Customer may decide to pay by combining multiple payment methods. What if two Customers decide to pay half each? What if the Customer pays with their gift card, and the rest in cash?
For that reason, rather than keeping everything together, an Order can be associated with X number of Payments. X, by the way, can be zero, because you need to contemplate the scenario where the Customer gets something for free (“Buy 10, get one free” kind of thing).
Decoupling Items from Products
Admins should be able to create Products in the system, and each Product may have multiple Variants (medium vs family size, gluten-free vs normal, extra cheese…).
But more importantly, admins should be able to change the Products they offer, and that shouldn’t affect past Orders. For example, you may decide that the family size Variant is no longer available for gluten-free pizzas, or that extra-cheese no longer makes sense for a Product called “SuperCheese”.
For that reason, past Orders shouldn’t reference Products you have available currently, but rather have all the information self-contained in an Item.
This is an extremely important idea: past Orders and Payments should be unaffected by future changes in Products and Variants.
The way we can model that is by having Orders reference, not Products directly, but point-in-time copies of those Products. We can call those self-contained copies Items.
An Order is therefore associated with multiple Items, with its specifics narrowed down with Variant (predefined) and Notes (ad-hoc).
In fact, these Items may have nothing to do with any Product whatsoever. We can leverage these and add extra charges to the Order, such as delivery fee, tips or taxes. We can also allow for the amount to be negative, which enables discounts.
Invoices Are Not Receipts
Every time an Order gets created or updated, a new Invoice gets created. Their purpose is to facilitate a transparent record of what is purchased, and what is requested from the Customer (the so called payment terms).
Invoices can be changed, but they don’t, generally. Maybe the pizza guy heard something incorrect on the phone, or maybe a cheeky customer tried to get away with an extra topping for free by requesting it on the Notes section. In that case, you can update the Order, and a new Invoice should reflect that change.
Receipts, though, are serious business. They are numbered, they cannot be changed, and are mandatory in most countries.
You may ask “aren’t Invoice and Receipt the same thing?”. They are not, and conflating the two may lead to problems down the line.
Here’s a good heuristic to differentiate an Invoice from a Receipt: an Invoice is a notification of Payment; a Receipt is proof of Payment.
While Invoices can be changed, don’t you ever think about changing a Receipt. In most payment applications, Receipts are immutable copies of Order and Payment data.
State Machines are fine for Orders, but are terrible for Payments
When dealing with money, having more information is better than less.
What I mean by that is that admins should be able to trace through a series of events to recreate what happened to a Payment. Statuses, for audit purposes, are never enough. Admins must sometimes demonstrate that the obligations towards the Customer were met (when someone disputes a transaction, for example).
Having this information is vital: it costs money if it’s not stored.
As a result, Payments should be referenced by a series of Events that stack up as the payment process goes along. If the Customer had insufficient funds, or if the refund failed, there should be an Event for each situation.
In practice, though, Payments also have status. That doesn’t mean that there is a column in the database specific for that. Instead, payment applications should determine that status based on the Events associated with that Payment. Having a consistent way of exposing the status of a Payment can be helpful when displaying that data on the screen, and for other services that are integrated with the payment application.
Recap
An Order can be associated with X number of Payments, where X can even be zero
an Order can be associated with multiple Items, which are self-contained copies of a Product
an Invoice is a notification of Payment, and it’s mutable; a Receipt is proof of Payment, and it’s immutable. Both are associated with an Order
An Order has status, which follows a State Machine; a Payment has Events, with are append-only
Unprofitable Complexity
We have just scratched the surface of what it takes to build useful payment applications. That doesn’t mean that real money software is much more complex than this.
It almost always is not more complex than this, and that is a good thing!
Take, for instance, open-air markets. There is all sort of bartering and swapping and deal making happening on those markets. My engineer side of the brain melts trying to model out all those special cases.
Most companies don’t account for that level of complexity. Engineers don’t want to incur in what Hillel Wayne called Edge Case Poisoning. That means the unbearable complexity added by trying to represent increasingly obtuse use cases. It’s not worth it to engineers.
It is also not worth it to the companies that pay those engineers. These extra use cases are simply not profitable. It’s OK for a company to give up on revenue if the costs of acquiring it are not tolerable.
That is the open secret of successful software architecture: you must not allow things you don't want to happen.