Choosing the right tools

Developing a modern FinTech backend: the pros and cons of popular approaches

FinTech is an industry oriented towards developing novel financial applications and solutions to traditional ways of dealing with finance. But building a FinTech application is no easy feat.

Today’s financial market is a true digital battlefield. Its landscape is vast, its opportunities are endless, and the weapons available to its warriors are varied. This battlefield is regulated by different authorities who always seem to be increasing the entry barrier for newcomers.The fact that you need to react quickly to the changes and requirements they impose to stay in the game is just icing on the cake. 

There are many ways of developing modern software applications. One of the biggest challenges that any development team faces is how to choose the right tools for doing so from the ocean of technologies out there.

Having built core banking solutions for solarisBank and been at the forefront of adopting popular approaches here at Mooncascade, I’d like to shed some light on how you can build winning fintech products without going crazy. I’ve included a list of pros and cons for each one, so you’re guaranteed to walk away from this article with some food for thought.

Keep in mind that a FinTech application always depends on the context and specific requirements it’s built for, and that there’s no such thing as a one-size-fits-all solution. Let’s have a look at some key terms you’ll need to be familiar with before we dive in

Terms

  • Monolith: usually a single-tiered, self-contained application that consists of multiple components / modules (UI, data access, domain logic) which allows for completing a set of business functions.
  • Microservice: a self-contained component of business functionality centered around one particular task.
  • Event Sourcing: a design pattern that allows for storing an application state as a sequence of events. In other words, not only does event sourcing enable you to get the final state (or state for some period) of the application, it also allows you to see the entire history of how the application has reached said state.
  • CQRS or Command Query Responsibility Segregation: a design pattern that stands for what it describes—separating the Command model (write/update state) and Read model (query, reports, read state) in your application. These models are usually meant to be different object models or services, typically running on different hardware.
  • DDD or Domain Driven Design: an approach to software development for building a rich model that reflects and follows business rules and implements business processes. It’s a very good tool for organizing lots of messy logic in complex domains.
  • Ubiquitous Language: one of the most important parts of DDD, in my opinion. This part forces all parties in product development (domain experts, developers, managers, etc.) to determine and follow the rigorous language of the domain model that is reflected in the names of processes, entities, actions, events, and so on.

Are you a Monolith or a Microservice?”

Monolithic Architecture

Despite the fact that many people have strong opinions about monolithic architecture — especially considering the recent boom in microservice-related topics—it’s usually the best way to start developing applications. Many big, successful services have started with monolithic architecture, including Paypal, Amazon.com, and Netflix. 

Here’s a non-exhaustive list of this approach’s pros and cons:

  • Pros: it’s a perfect fit when you don’t know the clear boundaries of your system’s components or how they interact with each other; it reduces operational overhead (sometimes significantly); it simplifies developing, testing, deployment, and the implementation of cross-cutting concerns (logging, monitoring, etc.).
  • Cons: scalability, performance, development velocity, long updates, reliability (code in one place breaks the whole app), potential size (number of files and amount of knowledge), difficulties in migrating to newer technologies; it breaks down modularity.

You’d be right to think that there are quite a few more cons than pros here. But with FinTech applications, it’s often important to get up-and-running as soon as possible, to “proof your concept” and get your first round of feedback from your client. Moreover, your initial requirements, goals, or even vision might be adjusted or shifted elsewhere as development progresses. To learn a bit more about why a monolith can be helpful in this context, I’d recommend reading Martin Fowler’s article “MonolithFirst”.

Microservice Architecture

Still quite a hot topic nowadays, there are lots of resources out there for using microservices. This is a more advanced architecture than the monolith. Adopting it typically requires an experienced team along with careful planning. But once a monolith reaches a certain size, it’s time to divide it into specific services. Here’s a list of some of this approach’s pros and cons (especially compared to monolithic ones):

  • Pros: independent scalability of services; continuous delivery and deployment; high system reliability; frequent updates and faster development; improved maintainability and testability; low cognitive load per service; capability of having autonomous teams that own one or more services; easy to onboard and test new technologies.
  • Cons: entails high operational complexity—you have to deal with things like inter-service and inter-team communication, partial failure, cross-cutting concerns (logging, monitoring), configurations, requests that span across multiple services; harder testing of interactions between services; hidden coupling; data consistency.

Another implicit problem with this approach is that startups tend to take it by default, aiming to solve problems that might not even exist in their use case. This in turn slows down development and could possibly damage a product or even business down the line.

For example, solarisBank, one of our clients, started their digital banking service with a monolithic architecture, then turned to microservices once they grew big enough.

Nevertheless, this approach provides a great opportunity for your product and business to scale, grow, and evolve over time. Plus, it enables implementing distributed event-driven architecture, and helps decouple your services even more.

Make your language ubiquitous

Domain Driven Design (DDD) is a popular approach to building software applications. I won’t take a deep dive into this topic since it’s a complex and broad one. Plus, it’s just as popular as microservices, so it would be easy to go wild. 

The main concept here — domain — is what defines your business. For example, solarisBank’s domain is digital banking; TransferWise’s domain is money transfers; but Amazon has multiple domains such as e-commerce, cloud web-services, video streaming, etc.

With DDD, you’ll use a set of the main building blocks from Tactical Design (Entities, Value Objects, Service Objects, Aggregates, etc.) and Strategic Design (Bounded Context, Context Maps, Ubiquitous Language).

One thing I’d like to elaborate on a bit is Ubiquitous Language (UL) and its importance. UL is a set of terms and concepts in the business domain relevant for a specific Bounded Context and developed with the agreement of all parties — the domain experts, the technical team, and others. It helps eliminate ambiguity, understand the business, and speak using the same “language.”

Ubiquitous Language must be expressed in the software model you’ll be using, and evolve along with the business and the model it’s applied to. The FinTech industry is really broad, so eliminating ambiguities at this stage will really help your team avoid getting lost as things move forward.

Here’s a simple example from the banking domain. Let’s take two (Bounded) contexts: Loans and Savings. The Loans context contains terms like Account (customer account), Interest (loan interest), Payment Due (loan payment date), and Overdraft. The Savings Context contains terms like Account (savings account), Interest (interest on savings), Payment Due (payment of the interest), and ISA.

Using similar terms without context and a shared understanding between teams can bring uncertainty to business decisions, break software, and affect the end user. In addition, the amount of time wasted on clarifications and “mental” mapping will only increase over time. By applying and enforcing UL within an organization, teams from different departments will be able to understand one another and tackle obstacles much more quickly. For instance, the support team might directly tell technical team members that customers complain about incorrect Interest calculation on their savings accounts (i.e., related to a Savings Context).

Source your events

The traditional way of making an application state persistent is to save it into storage. This means that you’ll always see the application’s final state. It’s a common way of dealing with data, especially if you have a CRUD monolith. As business grows and the team decides to split a monolith into different (micro-)services, questions about how to decouple these services and deal with distributed data (in case of using multiple databases or database per service) will start to pop up.

Furthemore, if a company wants to get a banking license or if it performs financial operations, it has to conform to regulatory and compliance requirements of the specific country or region in which it is operating. For example, the German Federal Financial Supervisory Authority requires an audit log of all operations taking place with customers’ accounts.

One of the ways to do this is Event Sourcing. It’s become popular due to increased interest in DDD and microservices. The essence of this design pattern is that changes to application states are stored as immutable sequences of events. Basically, it saves the history that leads to said state. By replaying these events, the state can be reconstructed completely or for an arbitrary period of time. Pro tip: use state snapshots to improve performance.

Usually, events represent domain-related facts that have taken place within a system. It might be a good idea to do Event Storming sessions in advance to identify these kinds of events. DDD also comes to the rescue here with the idea of Domain Events.

Take your bank account, for example. The bank doesn’t save your current balance. Instead, it stores deposits and withdrawals that happen over time:

  • DepositMoney(100);
  • DepositMoney(50);
  • WithdrawMoneyFromATM(25);
  • TransferMoney(100).

Your balance then gets calculated from this data. You can even request to see a list or total sum of all transactions during the last month.

Events are typically stored in what’s called an Event Store. It could be MongoDB, MySQL, Apache Kafka, etc. You can have different event stores for different subdomains (or Bounded Contexts)—for example, one event store for Account & KYC and another one for Payments.

What kind of benefits does implementing Event Sourcing provide?

Here’s a short list of some of the main ones:

  • All of the history relevant to your business is preserved;
  • Gives you the ability to determine the state of your system at any point in time;
  • Provides an audit log out-of-the-box that’s a natural fit for the FinTech industry’s regulations and compliance requirements;
  • Helps avoid the problem of storing and publishing an event in event-driven systems due to inherited atomicity;
  • Highly scalable reads (stay tuned);
  • Single source of truth;
  • Enables learning from the past, which is really important for identifying shortcomings and opportunities in a business or product;
  • Any kind of reporting, data analysis/mining, etc.;
  • Debugging capabilities

Of course, these benefits don’t come without a cost. This style of building applications is different and usually unfamiliar to developers, so it implies a learning curve. Here are some typical questions you should consider before diving into Event Sourcing:

  • Event versioning and backward compatibility; in short, events must always be backward-compatible;
  • Performance: in order to perform an action using the latest state, you need to replicate all of an entity’s events, though you can tackle it using snapshots;
  • Duplication of events in distributed systems: this can be especially important for FinTech apps, as you don’t want your customers having their accounts debited twice;
  • A decision to follow the Event Sourcing pattern should be made before or at early stages of the development process, because refactoring can be very tough;
  • Eventual consistency: the thing you’ll most likely reach with an ES pattern, because processing, reading, and reacting to events takes time.

Obviously, Event Sourcing is not a good fit for every application and can be overkill. But if you need traceable changes, audit logs, the ability to easily extend a system, different forms of reporting and analysis, and modelling of alternatives, then this is a solid solution. Plus, you don’t even have to implement ES for the whole system; it can be done for only some of its components.

Command & Conquer Query

Developers generally use the same models to read and update information when building CRUD applications. This simplifies and unifies how they work with data. Also, it’s easier to understand a mental model of creating, reading, updating, and deleting a record. That is, until your system and its requirements become more sophisticated.

You may need to make up virtual fields, like the number of days left to pay for a credit card, required for mobile app UIs only. Or a list of transactions for a specific date, noting the remaining amount of money after each transaction. Or you might want to update your information system based on specific business rules and data that allow only specific data within the model. Or you might want to store data that is completely different from the data you provide the system with.

This is where CQRS comes to the rescue. It’s a pattern that was initially described and popularized by Greg Young. CQRS stands for Command Query Responsibility Segregation. It’s related to the idea of Command Query Separation, or CQS.

So, what is CQRS? It’s a design pattern where the model that’s used to read information (Query) differs from the model that updates it (Command). The main purpose of the pattern is to decrease the complexity of dealing with data in sophisticated, distributed, and/or event-driven systems.

Imagine an investment application where a customer can buy some form of assets. An asset’s read model might contain a ticker name, company info, historical price data, current price, balance sheet, different ratios, with some of them being optional. In parallel, when the customer buys the asset, the write model might also require data like the customer’s account number and available balance, the asset ticker and current price, number of shares to acquire.

Read and write models can share the same database, in which case the database acts as the communication layer between the models. Another popular approach is to have separate databases for the models—a master database and read replicas—in which case there should be some communication mechanism between them, such as messaging/events. 

Benefits:

  • Helps deal with complex systems;
  • Separation of concerns with simpler read/write models;
  • Independent scaling of read and write models (services);
  • Shapes data for different purposes (especially for read models);
  • Naturally fits event-driven architectures (typically with Event Sourcing).

Drawbacks:

  • Added complexity might reduce productivity and increase risks;
  • Operational and maintenance overhead;
  • Potential code duplication;
  • Eventually consistent views, though this might be fine for some domains.

Friendship of CQRS and Event Sourcing

As noted, CQRS fits event-driven architectures, and Event Sourcing in particular, very well. The reason for this is that CQRS helps resolve problems that come with ES, like performance, events processing, and complexity. 

In general, the read model consists of (query) services that expose APIs to get the info and projectors that consume events and update databases (project the state of events). As an event store in ES is a source of truth, it allows us to replicate events and generate different views for different purposes. Also, you can easily remove and replace query services if requirements get changed and new query services get shipped.

The write model typically consists of command services and reactors. The command services accept and validate requests/commands, build an Aggregate (reference to DDD), check invariants, and then generate and store events in the event store. The reactors basically do the same, apart from being triggered by an event other than command, i.e., they react to events and might produce new events. Commands can be processed immediately or can be placed into a queue, i.e., they can be processed synchronously or asynchronously.

A couple of words about aggregates, with reference to Martin Fowler’s description: an aggregate is a collection of entities and value objects that can be treated as a single unit. It has one main object from the collection as an aggregate root. All of the external interactions with an aggregate from outside pass through the aggregate root. The purpose of the root is to keep data integrity, consistency, and check business invariants. Transactions shouldn’t cross the aggregate’s boundaries. Aggregates are stored to and loaded from storage as a whole. For example, a bank might have the following requirements in order to open an account: your age should be 18+ and you should be a resident of the UK. These are clear business invariants that must be enforced by the account aggregate.

Here are some questions to consider when using the combined CQRS + ES approach:

  • How to deal with duplicated events if your message broker distributes them multiple times;
  • How to implement idempotency for commands;
  • Does your business domain accept eventual consistency?
  • How to react in case of event replication. Should your system notify third parties?
  • How to avoid event cycling;
  • How to distribute events and where to store them;
  • How to enforce the event schema;
  • How to version events;
  • And many more…

NewCommand(FinishArticle)

Nowadays, building software requires you to make a host of business-related, operational, and technical decisions. If you ask an experienced developer how you should make these decisions, the most common answer you’ll hear is: “it depends.” Your decisions should always depend on context, requirements, capabilities, expertise, experience, and money. But starting simply, improving iteratively, seeking feedback quickly, and accepting trade-offs within the technologies you use will all help your product grow to impressive heights.

Building a FinTech product yourself?

Building next-level FinTech products in an increasingly unpredictable, demanding, and regulation-heavy market is an uphill struggle. The market waits for nobody, so if you want a chance at winning, you’d better know how to play its game. You’ll want to join forces with someone who can give you an edge over the competition. An experienced FinTech partner with a proven track record can do just that, by taking a big part of the product development load off your shoulders. They can make sure your platforms are built with cutting edge technology and the highest standards in security and usability, giving you time to focus on the rest of your business. Here at Mooncascade, that’s exactly how we help our clients, whether they’re a startup or an established player. So why not drop us a line to see how we can help?

Useful Links & Statistics

Published by Roman Shandurenko

. Roman is a Software Engineer whose main area of focus is backend development. He's a proactive, enthusiastic, and open-minded person with a passion for modern technologies. At Mooncascade, he develops and delivers customer-oriented solutions with high quality in mind and a belief that software should bring value without ever becoming a burden.