Evolution of payments

Anton V Goldberg
4 min readSep 21, 2020

Today’s post is about one of the projects we are working on at Upwork. The goal of the project is to modernize payments so there is less work when there’s a new product that requires payments’ involvement and users (freelancers and clients) get less payment-related errors. To fully explain the project I need to dig through a bit of history. I’m going to discuss various implementations of payment systems below and while doing that gloss over most of the details. For our purpose there are only 3 areas I’ll pay any attention to: API, synchronicity of payment, and location of the accounting.

Traditionally any website accepting payments begins with the API that is really close to what we see on a credit card statement: payer, payee, product description, and amount. The implementation is synchronous in a sense that after a customer clicks the final “OK” button the web page calls the backend and the backend within that same thread calls the payments processor to charge the customer. The result of payment processing is returned in the same call.

The scheme has many advantages: it has clearly defined failure modes, it’s simple and if the payment fails the customer is there to fix the problem. Payments processor (such as Chase Paymentech, for example) typically does the necessary accounting and sends reports and data extracts back to the company for import into the company’s accounting system.

During the next stage of growth the interface becomes more complex as it needs to include the fields necessary for the routing of the request to the proper provider and helping the provider do the accounting. The API still stays synchronous (the customer still waits on the web page) until the payment is done. At this point synchronicity becomes a problem because the system is more complex, fails more often. More providers are involved and each of them fails independently, the internal routing is also more complex and its internal failures add to the mix. Some payment methods are asynchronous by nature and synchronous systems have real issues dealing with that as the payment can fail after initial “success” but the customer is long gone by then. The reason the system stays synchronous is simple: changing that requires a lot of rework across back- and front-end, while errors and inconveniences aren’t that bad yet.

At the next stage of growth the website reaches the point where it “sells” multiple different products with different accounting for each. Outsourcing accounting to payment processors is no longer the default option. The interface becomes even more complex because now every transaction has to carry its accounting meta-data. Rolling out a new product anywhere on the website now requires 3-sided conversations between the business team, accounting, and payments. The latter two teams become the bottleneck. The amount of transactions (the traffic) reaches the point where failures due to synchronicity generate enough complaints to attract everyone’s attention. And that’s where Upwork sort of finds itself now.

The project seeks to sidestep the whole “overgrown transactional API + synchronous processing” problem by elevating the interface to the next level of abstraction. Let’s call it “Order”. The goal of that API is to describe to payments what business product it needs to charge for and who the customer is. Then payments take over. Internal workflow checks the state of the customer, their payment instruments, and preferences. Payments-owned UI takes over if needed and continues the dialog with the customer until Payments collects all the necessary information. Then Payments can execute the actual transaction according to the requirements of the product, nature of the payment instrument, and the current state of the system. Order API will include separate extensible facets describing various parts of Order. There is a facet for accounting, describing the product from the business standpoint, thus eliminating payments and accounting as a bottleneck. There is a facet describing the product’s behavior from a payments standpoint. For example, data for subscription differs from one time charge. There are as many facets as needed and their number and content can change as the company grows without affecting Payment Systems or its callers. We can now deprecate the transactional API and rework the transactional layer at relative leisure.

Clearly, this approach hasn’t been invented at Upwork and there are many examples of similar implementations. We are just happy we found how to eliminate the bottlenecks by removing Payments organization from the loop for each new product.