Skip to Content

Delightful Payments

Monzo Payments Get a Modern Makeover

Over the years, the payments flow for bank transfers has started looking very different from the flow we use for Monzo-to-Monzo payments (what we call Peer-to-Peer payments). Despite still being functional & secure, those flows were no longer fully meeting the needs of our 9m+ customers who wanted to easily change payment sources inside the flow, review and confirm every step of the payment process, be informed in a timely manner when there are warnings and guided on how to fix them. Behind the scenes, there was also a lot of legacy code and platform disparity that made making new changes more challenging.

The Core Experience team started discussing how to elevate the payments experience and make it really delightful. Not just an improved UI, but addressing each of those pain points at the root and setting scalable engineering & product foundations for the years ahead. After about 8 months of development and testing, we have now rolled out everything we built - it has been a one-of-a-kind experience!

Engineers working alongside product and design

From the very early days, we realised that working sequentially was not an option for us - i.e. engineering couldn’t only start after product requirements were fully complete - because there were so many different angles to approach this project from and we could only achieve success if all of the disciplines worked together.

A few engineers worked with our product manager and lead product designer to start setting out low-fidelity prototypes of what we want to deliver. We also mapped countless edge cases in the existing flows that needed careful consideration to ensure we didn’t change things for the worse.

This took about a month or so, but we ended up with high-level engineering, product and design plans and were ready to start getting more engineers involved to start building. We knew that the plan was only 80% complete, but that was all we needed to get started.

Backend vs client-driven

One major decision we made was how much to control from the backend versus in the mobile clients. Relying heavily on the clients would give us smooth, fast transitions and less latency, but it would slow down our ability to iterate quickly due to our weekly release cycle. Conversely, putting too much on the backend would allow us to iterate faster but result in a less engaging user experience, particularly when internet connectivity was poor.

We determined that secure functions, like eligibility checks and payment processing, needed to be handled on the backend. However, tasks such as input validation and determining which screens follow each other (i.e. transitions) could be managed by the clients to improve the user experience.

In the end, we adopted a hybrid approach. We kept presentation logic on the clients and core functionality in the backend. Additionally, we used data already available on the clients, like the user's account list, to avoid unnecessary network requests and speed up new features. This helped us balance between a fast, delightful user experience and the ability to quickly iterate on the new flow.

For the new endpoints that we built, we distinguished between blocking and non-blocking calls so that we don’t block the whole payments experience when a non-critical issue such as being unable to load amount suggestions happens.

Unifying payment flows

We strongly believe that our customers shouldn’t have to worry about technicalities like exactly what kind of payment they’re making so we wanted to unify bank transfers and Monzo-to-Monzo payments into a singular flow. This required pragmatic abstractions on both the backend and the clients that were both simple to get started, but powerful enough to easily extend to more payment types in the future.


First image shows payment input screen with a reference input field in focus. Second image shows payment review modal showing amount, source and destination fields.

On clients, we started by defining the right elements to have on-screen and their order:

  1. Navigation bar with an option to schedule payments

  2. Amount input

  3. Reference input

  4. Source selector

  5. Destination selector

  6. Payment review button

We maintain the same order regardless of the payment type as these are the inputs needed for all payments.

To deliver a continuous flow without over-complicating transitions between different screens, we created separate native screens for each part of the flow that re-use the same UI elements. We also made great use of bottom-sheet modals that go over the existing screens when we needed to deliver extra contextual information such as confirming payment details.


We didn’t want to have separate APIs for different types of payments and invested in abstractions to centralise shared logic. These allow us to use tight interface-level and custom validation rules to make sure that we never end up trying to process an invalid source or destination in error cases where the client sends bad payment requests.

Better representation of Confirmation of Payee outcomes

First screen shows payee details screen showing an error on account full name because of not getting a full match. Second screen shows payee details screen showing an error on account full name because of not getting a full match. Third screen shows payee details screen showing an error on account full name because of not getting a full match.

Confirmation of Payee (CoP) is an account name checking service that can help assure users that payments are going to their intended recipients as well as showing warnings if there is any chance of misdirection. Although our old bank transfers flow was making CoP requests too, there was no positive confirmation when all the details matched and we often asked users to manually edit details.

We built a brand new flow powered by a new API endpoint that takes exactly the same inputs as the old flow, but returns a much richer response for the clients to render. On top of returning user-friendly status messages, we implemented an abstraction that enables us to easily control what information we display for each individual field. For example, when a customer types a slightly incorrect name, we could easily signal this on the UI and make it easy for them to change.

Visualisation of how backend response gets used on mobile clients to render Confirmation of Payee outcome.

We also built smart suggestions that the clients can apply if a customer wants to go ahead with our suggestions rather than manually editing each field. We display these suggestions when we’re certain that the recipient type or the recipient name has been entered slightly incorrect and changing these would result in a full-match outcome. We don’t infer any types or names on our side, and simply use the outcomes provided to us by the Confirmation of Payee response.

Strong observability

Despite our changes not touching core payments logic such as the Ledger, we still wanted to have strong observability throughout the whole flow - from the moment we display our new flow to when the payment is successfully complete. There was not a one-size-fits-all solution and these are just a few things we deployed to improve our observability:

  1. We duplicated the alerts of the core payments services with reduced severity levels and redirected those duplicates to our own channels so that we quickly get notified if there is an issue such as bank transfers failing and debug to see if it is caused by our changes.

  2. We defined an extensive list of backend metrics for each part of the flow such as number of failed payments, processing times for successful & failed requests. These metrics were complementary to the default platform metrics that we get out of the box for each service and allowed us to build rich monitoring dashboards.

  3. We adopted a principle of favouring extra-noise over missing critical alerts by setting custom platform alerts such as payment requests taking longer than expected, elevation in errors or processing less payments than usual.

Collecting feedback and iterating

Different stages of feedback collection and iteration throughout the project.

Instead of following a linear process for the rollout, we favoured getting as much feedback as early as possible. This included getting the community engaged, shipping small changes to collect feedback from staff users and experimenting with different variants when we needed that extra bit of certainty to know what is the best experience for our customers. 


Once we completed the full round of testing and collected as much feedback as we could, we shifted our focus to staged rollouts where we go to a certain % of our customers, monitor everything before we increase that % and eventually hitting 100% 🎉

As eligibility is determined by the backend, we could easily update how and when we show new changes to our customers without having to wait for the client releases. As far as clients were concerned, they showed the new flow if a user was eligible and the old flow if they were not eligible

message Eligibility {

    bool eligible = 1;

    string ineligibility_reason = 2;



Having seen the successful rollout of the new payment flows without any major incidents, it is safe to say that the key pillars behind the success were having well-defined product requirements, a clear picture of the current state, and strong levels of observability and continuous feedback cycles. All of these combined allowed us to make pragmatic decisions when needed and incrementally improve the product whilst minimising the risks. We’re incredibly proud of what we’ve built, and are really excited to see our customers using and enjoying it!