Receiving international transfers at Qonto: our SWIFT architecture
By: Wagner Amaral
Tags: Tech, Software Architecture, Kafka

Earlier in 2020, Qonto introduced to its customers the ability to receive money transfers from all over the world. How did we accomplish that?

SWIFT

Let’s begin with a quick definition of something which might sound familiar: The Society for Worldwide Interbank Financial Telecommunication (SWIFT) is the cooperative behind the SWIFT network, the worldwide standard for communication between financial institutions. For Qonto, being connected to this network means we’re accessible to banks all around the world. For our customers, it means easily receiving money from different countries, in several currencies, and have it turn up as Euros in their accounts.

But how does it work? How can one become a part of this network?

The graph network

Being a part of the SWIFT network means establishing partnerships with other participants in the network. To send or receive messages over the network, you must have an agreement with one or multiple partners, and you only communicate directly with them — even though all messages go through the same network. We can imagine the network topology as an undirected graph:

Each participant in the network is identified by an unique code issued by the SWIFT cooperative, called Business Identifier Code or BIC for short (sometimes also called SWIFT code).

As part of the SWIFT infrastructure, there is an official registry called SWIFTRef serving as a directory of BICs. After you register your BIC on SWIFTRef, you have the ability to publish the list of your direct partners, and by doing that you become reachable to other participants in the network.

With that in place, Qonto does not need a direct connection to every single participant. In the previous example, if Bank 6 wants to send money to Qonto, they can choose the following path through the network:

Qonto’s architecture

In order to build a scalable and robust system, we set two goals while designing Qonto’s architecture to connect to the SWIFT network:

Being able to add a new network partner without requiring the time and effort of building a new system

Why would we want another partner? In the SWIFT network, depending on the type of transfer, all the intermediary participants can charge a fee for passing along the information. By having more direct partners, we increase the chance of a transfer having fewer intermediaries, reducing the fees charged on the transfer.

Convert the data received from the network as early as possible, and never allow network data to leak into our systems

One reason why this is so important: SWIFT was founded in 1973, and while they have a new message format, the original format is still the most commonly used. It’s not a very friendly format to parse, and it can be quite challenging to find the information you need. Here, try it yourself:

{1:F01BNK1DEBEAXXX1258143442}{2:I103QNTOFRP1XXXXN1}{3:{119:STP}}{4:

:20:1987238234

:23B:CRED

:32A:201007EUR1234567,89

:50A:/DE10500105176467386842

JOHN DOE

32 MAIN STREET

BERLIN

:59:/FR1210096000509958666734W89

JANE DOE

22 RUE DE PARIS

PARIS

:71A:OUR

-}{5:{MAC:00000000}{PDE:}}{S:{SAC:}{COP:P}}

With those goals in mind, here’s the solution we designed:

Let’s get to those layers in more detail.

The connector layer

This layer is responsible for connecting to a partner, sending and receiving messages, and handling network failures (e.g. retry at a later time if the partner is unreachable).

We store a raw copy of every message that passes through this layer in a bucket in AWS S3 for safekeeping and replayability. We can think of the S3 bucket internally as a mirror of the messages we exchanged.

Whenever a new message is received from the network, it is published into a Kafka topic for the other layers to consume. Whenever a new message is sent by the other layers (into another Kafka topic), it is forwarded to our partner. And that’s where this layer’s responsibility ends.

This layer takes care of the first goal: the effort of adding a new network partner is scoped to creating a new connector in this layer, while keeping changes to the rest of the system minimal.

The parser layer

The scope here is very small and straight-forward: converting between those nasty SWIFT messages and our own internal representation, the format which the rest of our systems can understand easily.

This layer is stateless (there are no databases). It receives data as input, and produces data as output. Aside from the I/O of reading and writing to Kafka, the entire codebase is basically a pure function. And since it’s Kafka, we can even choose to implement it directly as a stream processor.

This layer takes care of the second goal: the rest of the system never have to deal with the complicated network data. Here’s the above example, after being converted by the parser:

{

"sender_bic": "BNK1DEBE",

"sender_account": "DE10500105176467386842",

"sender_name": "JOHN DOE",

"sender_address": "32 MAIN STREET

BERLIN",

"beneficiary_bic": "QNTOFRP1",

"beneficiary_account": "FR1210096000509958666734W89",

"beneficiary_name": "JANE DOE",

"beneficiary_address": "22 RUE DE PARIS

PARIS",

"amount": "1234567,89",

"currency": "EUR",

"value_date": "2020-10-07"

}

You can look back at the original message and see where the relevant data was originally stored. Our services surely have a much easier time working with the parsed data.

The operational layer

And finally, this is where we handle the transfers themselves: checking if the operation is valid and permitted, crediting the customer’s account, and triggering notifications.

This layer does not need to know about the network connection, or the structure of the SWIFT messages. It has access to a simple set of normalised data that can be easily manipulated, reducing considerably its scope and the occurrence of bugs.

Does it sound familiar?

If you squint, you might notice some similarities with the hexagonal software architecture. In fact our design was inspired by that architecture, except instead of code, functions, namespaces, we are dealing with microservices and Kafka topics.

The connector layer is the port, and the parser layer is the adapter, and everything is abstracted away from the operational layer (the innermost layer in the hexagonal architecture). We get the same benefits where our services are decoupled, controlled by the data flow, and each individual piece ends up being easier to reason about on its own.

Conclusion

Good engineering practices often find their way to help in areas where one might not expect. By applying the principles of layering and separation of concerns to our microservices architecture, we were able to simplify our reasoning about the whole product. Working with smaller logical pieces and straightforward data, Qonto can provide a more robust system and a better experience for all our customers.