Back in April 2023, we launched the ability for you to receive Euros into your Monzo account using your International Bank Account Number (IBAN). Earlier this year, we expanded this with over 40 additional currencies to allow you to receive money from all over the world. To make this happen, we built our new modular International Payment Processing system from scratch and rolled it out to customers two weeks ago.
💸 What is an International Payment?
We define an International Payment as a bank transfer where the origination or destination country or currency is different to that of the account it is being sent to.
For example, if your friend who had a bank account in Australia wanted to send money to your Monzo account, this would be an International Payment because money is travelling across the world. It would also need to be converted from Australian Dollars to Great British Pounds along the way.
📤 How do we send an International Payment?
To send an International Payment, you need your IBAN. This is your International Bank Account Number. If you have a Monzo account in the UK, your IBAN will look something like GB08MONZ04000412345678. This may look a little random, but it has everything in it to tell the sending bank how to get a payment to your Monzo account. An IBAN consists of three parts:
GB is the 2 letter country code for Great Britain.
08 are check digits, also known as a checksum.
A checksum is a calculation that is used to validate whether the rest of the IBAN is correct. If you mistyped part of your account number or sort code, the sending bank should prevent you from sending the payment because they would not sum to the checksum digits at the start.
Fun fact: the last digit of your account number is also a checksum. When paired with your sort code, we can calculate whether an account number is valid for a given sort code.
The rest of your IBAN is your Basic Bank Account Number (BBAN).
The format of BBANs changes based on the country, but for the UK, its split into three parts
MONZ is Monzo’s Bank Code
040004 is your UK sort code
12345678 is your account number
The format of IBANs differs based on the country of the account you are sending funds to. Some banks may also ask you for a BIC - a Business Identification Code. This is an additional way to direct the payment to make sure it arrives at Monzo. Think of it like a post code or ZIP code for your IBAN. You can find Monzo’s BIC in the app alongside your IBAN, but at time of writing, it's MONZGB2L for payments to UK accounts.
With your IBAN and BIC, the sending bank can send Monzo a message over the SWIFT Banking Network that tells us who sent the payment, how much money to credit your account, and much more.
But there’s one small problem in our example, what if the sending bank account is using a different currency from your Monzo account? Enter the world of currency exchange!
💱 Foreign Exchange
For money to move between two banks across the world, they both need a common currency that they can use. Foreign Exchange, also called FX or currency conversion, is the process of two entities trading one currency for another where one of the entities needs to be able to trade in both currencies.
For example, if you exchange currency before going abroad, you have Great British Pounds and a shop has Great British Pounds and Euros. The shop can trade you one currency for another because it has both currencies and value of how much one is worth of the other.
This same process happens between banks. Both banks need to be able to move money in a common currency between their own accounts. If Monzo wants to be able to receive Australian Dollars, we need to be able to either accept Australian Dollars, or the sending bank needs to give us Great British Pounds. To provide the best experience possible, Monzo can now accept over 40 currencies.
But wait… you now support over 40 different currencies. Does that mean Monzo has over 40 accounts in different countries and currencies, and has to manage and trade all those currencies?
Not quite, and this is where Correspondent Banking enters the scene!
🏦 Correspondent Banking
A correspondent bank is one that can receive payments on our behalf using their network of banks and partners around the world. The advantage of this is not only can we support a large number of currencies, but also payments can take a more direct path to Monzo. This speeds up how quickly we receive them and can also reduce fees.
When the sending bank sends Monzo a payment, they can look at the SWIFT Standing Settlement Instructions (SSIs) database to see how to route it to Monzo. We list on the SSIs database that our partner can accept payments in all of the currencies we support on our behalf, and the sending bank routes the payment via our partner.
Our partner receives the payment message, performs the currency exchange and sends us a message over SWIFT with the details of the payment, including your IBAN, the amount to credit you and the exchange rate.
The world of International Payments is complex and has a lot of moving parts. We built a new International Payment Processing System to make this entire experience seamless to our customers. Let’s take a technical dive into how we process International Payments at Monzo.
International Payment System
As part of building this for customers, we took a different approach to implementing the processing system for International Payments. We have many different payment processing systems at Monzo and have learnt many lessons over the past eight years. We wanted to design a system that followed three core principles:
1️⃣ Correctness
As with all payment processing, we have to ensure that every payment is processed correctly: customers must receive the correct amount into their account, on the day they expect it and be shown the correct details in the app. The International Payments system is no exception, and we perform checks when processing to ensure the result of a payment happens once and exactly once through extensive use of idempotency in our decision making and when applying the payment to an account. These are backed up by our reconciliation and coherence controls as Joost describes in Safely processing payments at scale.
2️⃣ Test-ability
There are a lot of steps that we need to do before we add money to an account. We’ll dive into some of these later, but this can become particularly challenging to test complex behaviours. We’ve experienced unit tests getting incredibly long in the past because we hadn’t got the level of abstraction correct, and this made writing and testing changes more difficult than it needed to be. As a principle, if you feel like your test needs its own test, then your test probably needs rethinking.
We wanted to take a different approach with the International Payment Processor architecture and implementation that had a distinct focus on developer experience. The easier it is for engineers to write tests, the quicker it becomes and this allows us to test more edge cases and have better coverage of our code.
3️⃣ Scalability & Reusability
When we think of scalability as engineers, we typically think of ‘how many requests or events can we process per second’. This was definitely a consideration for this system but this isn’t the type of scalability we prioritised when architecting the processor.
As I mentioned earlier, we’ve built a lot of different payment processors over the years. This has resulted in each type of payment having its own connections, message storage, processor, lifecycles, reconciliation, coherence, events, metrics, alerts, runbooks and more. All of these things are required for us to safely process customer payments. These things take time to be built and tested, and largely look similar between different types of payments.
We therefore want to allow us to onboard new partners and new payment types in the future without having to build a brand new processing system each time. This led us to build our adaptor based architecture for International Payments. As a positive side effect, the more similar we can make our payments processing between types of payments, the easier it is for new engineers to get up to speed.
Processing Payments
First up, let's explore the four core stages of processing a payment once we have created a payment:
1️⃣ Payment Creation
Upon receiving a payment message, we need to store, parse and validate its contents. We do this within an Adaptor. An Adaptor is responsible for taking the raw payment message we receive and marshalling it into our common representation of a payment, suitable for our payment processor. Different partners have different ways of sending us information in a variety of different formats. Decoupling the raw payment message from the payment processing allows us to build downstream logic against a common interface.
Adaptors also allow our processor to interface back to the partner that sent us the message. For example, if we decide to return a payment, the processor can call the ReturnPayment() method on the adapter without needing to know the specifics of how to return the payment. This simplifies reasoning about the processor's core flow and makes writing tests easier as we can use mocks to ensure adaptor methods are called, without having to mock individual requests to other services.
An adaptor within the processor is typically complemented by a separate microservice that takes care of the domain specific behaviour of that partner and keeps our processor lightweight.
When building the Payment, the adaptor is also responsible for adding the necessary decisions and effects to process the payment. These terms concepts should all become clear in the next few sections 🙏
2️⃣ Decisioning
Once we have constructed a payment object, we need to decide if we are going to accept or reject the payment based on a number of different conditions. These could be:
Do we have an account for the IBAN that’s in the SWIFT message?
Is the account open?
Is the user within their payment limits?
Has the payment passed compliance/Financial Crime checks?
In the International Payments Processor, these are called Deciders. Each decider must conform to an interface where they use the common representation of a payment and must return an Outcome or an error. Deciders can be reused across different types of payments and can be tested easily because they are using the common representation: The available outcomes for a decider are:
Accept - the decider accepts the payment, or has no effect on the payment
Reject - the decider rejects the payment and we should not credit it to a customer
Hold - the decider needs more information and cannot make a final outcome
We then cycle through the list of deciders for a given payment and evaluate each one. To determine how to proceed with processing, there is an order of precedence with outcomes to generate an Overall Outcome:
If any decider returns Hold, then do not proceed with processing
If any decider returns Reject, reject the payment
If all deciders return Accept, accept the payment
Once we have an Overall Outcome, we can decide whether to continue processing.
For an accept or reject outcome, we persist the individual deciders outcome and continue to Effect Generation.
For a hold outcome, we stop processing the payment and wait for the Decider to make a final outcome. This can be achieved via an external system re-triggering processing after making a decision.
3️⃣ Effect Generation
An ‘effect’ is something that happens as a result of us processing a payment. Each payment typically generates a number of effects in order to apply it to your account. Some examples include:
make a money movement within our ledger to record you have received the amount
sending a message back to the sending bank to say we’ve received the payment
sending a notification to you to say your money has arrived
adding the payment to be moved between Monzo’s internal accounts
Which Effectors are generated and how they are configured is dependent on the type of payment and the Overall Outcome from the decisioning stage.
4️⃣ Effect Application
Once we have generated a list of Effectors, they are then applied. This could include making requests to other services on our platform, generating payment messages, emitting events, and more. Each Effector must conform to a given interface that allows it to return metadata about what it has done, or an error if something has gone wrong. This is important as we need a full log of everything that has happened to a payment, both for auditing purposes but also debugging in case something goes wrong.
Effect application can be unit tested by configuring each effect and applying it. We can mock the request and responses to ensure that it’s using its configured values.
🤝 Adding new partners and payments
The result of this level of modularisation within the processor is that we can very quickly add new partners and payment types into our processor.
For a new partner, we can write an adaptor and microservice that understands how to communicate with the partner
For a new type of payment, we can reuse the existing deciders and effectors, configuring the inputs to effectors within the payment object.
Additionally, any controls, dashboards, alerts, metrics and events we add to the core processor are immediately available for all partners and payments.
Testing Approach
Before any changes make their way to production, we need to have strong confidence that they work as expected. We take advantage of multiple levels of testing to achieve this, whilst creating a great developer experience and guaranteeing correctness within our processor.
We use unit tests to ensure that each individual component is working as expected and the control flow of the processor is functioning as expected:
Adaptors are tested to ensure that payments are built with the correct fields, the expected list of the decisions and the correct set of effects.
Payments are configuring the effects correctly based on the outcome and fields within the payment
The behaviour of Deciders can be unit tested individually by constructing a payment object, trigger evaluation, mocking requests and observing the outcome
The overall process of evaluating deciders and the overall outcome is correct by using a mock decider that returns a fixed outcome
Effector makes the correct requests based on their configuration
The overall process of effect application applies every effect and persists its metadata correctly
We explicitly decouple testing control flows and individual components behaviours to ensure the tests are highly specific and easy to reason about. This emphasises our commitments to the three core principles we defined earlier: small components increase reusability and aid in test-ability, which give us further confidence in our correctness of the system.
We extend unit tests with acceptance tests for each type of payment. We treat the system as a closed box by initiating a payment from an example message and observe the effects that happen as a result. This tests the real world behaviour of the system without having to mock a large number of requests and responses in unit tests.
These acceptance tests also cover how our system interacts with other systems on the platform, also known as the gaps between services. Mocked requests and responses are only as good as the last time they were updated. With acceptance tests being run on a schedule within our staging environment, our system is being tested against the latest version of all the other services it interacts with. This gives us strong confidence we will not disrupt processing when deploying changes to production.
Wrapping it all up
In both International Payments and payment processing in general, there are lots of moving parts for a payment to arrive in an account. Whilst we make this seamless to customers, we are also improving the experience our engineers have whilst adding new functionality, fixing bugs and testing our processing system. We are continuously striving to make our systems safer within Monzo and reducing the time and engineering effort it takes to launch new types of payments in safe and controlled way.
If you found this deep dive into payment processing interesting, and want to tackle these kinds of challenges on a daily basis, we're hiring for Senior Backend Engineers, Staff Engineers and Engineering Managers!