This content originally appeared on Level Up Coding - Medium and was authored by Kevin Vogel
Simple Bank API based on Domain-Driven Design, CQRS, and Event-Sourcing
Recently, I published three articles, each on Domain-Driven Design (DDD), CQRS, and event sourcing. In each of these articles, I have made it clear that while these concepts and architectures are independent, they complement each other perfectly, especially in the context of microservices and APIs.
However, I would like to go into this interaction more closely today. I created a simple open-source application for this, primarily written in TypeScript.
I wrote a bank account API consisting of two microservices and an API gateway. I implemented the microservices in TypeScript and with NestJS. But, I wrote the API Gateway in Go just for fun.
I seamlessly combined DDD, CQRS, and event sourcing in this project. Since there was no official event sourcing module for NestJS, I wrote one myself and made it available on NPM. Better don’t use it. It’s not field-tested.
Technical Features
- Domain-Driven Design, CQRS, and Event-Sourcing combined
- 2 NestJS microservices as Monorepo, split between commands and queries
- API Gateway in Go (HTTP to gRPC)
- created an NPM package for Event-Sourcing in the NestJS ecosystem
- using Kafka as an asynchronous event stream
- using gRPC as a synchronous transport protocol
- Database per service pattern
- ensuring idempotency due to eventual consistency
- NoSQL approach with MongoDB as write-database
- SQL approach with PostgreSQL as read-database
- storing Proto files in a shared repository
- everything dockerized but applications
- simplified usage of Sagas
Before we start with the actual article: I recommend that you read the articles mentioned beforehand, as I will not detail each concept again in this article.
Also, before we talk about the application itself, let’s quickly refresh our knowledge about the combination of DDD, CQRS, and event sourcing.
DDD, CQRS, and Event Sourcing in Theory
As we know, everything in CQRS starts with a command. A command is an intention to bring a change in the system. This goes very well with the term “command” in DDD. A command in DDD is also about the same intention to make a change. A command in DDD is the trigger for an aggregate that ultimately aims at a change.
In this interaction between CQRS and DDD, CQRS integrates the so-called queues (in this case, the command queue or command bus) so that we create a command and transfer it to the command queue, resulting in an aggregate.
In order to process this command, this aggregate requires an internal state. So you need the current status of things, mostly from the database. But this is precisely where event sourcing comes into play. Because with event sourcing, this state can be restored with the data from the past.
For reasons of traceability and consistency, event sourcing is a perfect fit in a bank account API, e.g., to be able to trace the balance of a bank account at any time, since with event sourcing, you have saved every change in the so-called event store. With event sourcing, you can, so to speak, travel back in time at any time.
The command then leads to new domain events being generated, and these domain events are then added to the event store and are thus part of the internal state for the following command of this type. So you could think of a cycle.
The aggregate, which can also be seen as a kind of container for several tasks, can also forward the domain event to an events API so that these domain events can also be processed in other ways. Think of push notifications to the client to inform that client about the result of the command, or other further processing, for example, through other microservices of the internal application infrastructure. There are no limits, and it differs from application to application.
However, another essential task of the aggregate, in conjunction with CQRS and event sourcing, is to store the result in the view/read database or initiate this process. This result or this database is then later accessed via the queries.
A view is nothing more than a pre-calculated view or calculated data. Every time a domain event happens, i.e., if a technical event has taken place in the business logic, it usually affects one or the other view and thus on the part of the queries of CQRS.
Or put, when a Command occurs, the resulting domain event is stored in the Write database, but the result of that event is eventually updated in the View/Read database.
This update process then follows the CRUD logic we are familiar with, i.e., there will be views where, in the event of a particular event, a new entry will need to be added, there will be views where an entry needs to be removed, and there will be views where an entry needs to be updated. However, as mentioned, this only takes place in the query part of CQRS, where event sourcing is not used in the standard.
As a simple example, if the user sends a “TransferFundsCommand” to transfer funds to another account, it will become a “FundsTransferredEvent” and that event will just be stored in the event store. This event is now also passed on to the view/read databases, where this event is then interpreted accordingly.
The balance of the initialized bank account is reduced in the view. At the same time, another bank account has to receive the money, so another command has to be triggered. There are often additional logs that can also be triggered.
A mapping of the following tasks is thus created, which are gradually interpreted and processed. And this business logic on the view/read side is, of course, very different from application to application. There are components in CQRS for this, and these are called projections.
At this point, it has to be decided which domain event is relevant for which view. However, it should be noted here that the view is not the single source of truth, namely the event store, since the current status of a command can be reproduced there at any time.
It is essential, and what you have to pay attention to this projection naturally decouples writing into the event store from writing into the views store from which reading is carried out afterward.
This means that these write processes occur with a slight delay, which means that there is no immediate consistency of the data between write and read from a technical point of view. This is referred to as eventual consistency. The consistency is thus established but only slightly delayed. This may sound strange at first, but this is not a problem in 99% of use cases.
Event sourcing does not take place on the part of the CQRS query in the standard, which is why this interaction always refers to the part of the command.
Project Description
As I mentioned, it’s a small bank account API on distributed applications. The idea is to open and close a bank account and deposit, withdraw and transfer funds. So the application idea couldn’t be more simple.
However, the approach and the concepts behind this project are in focus. I chose to go for a combination of Domain-Driven Design, CQRS, and Event-Sourcing. These three different concepts are great in combination.
DDD and CQRS are not unusual when working with Microservices, especially in the Java universe. Event-Sourcing makes sense if you want to get a history of your data because, in Event-Sourcing, you save every request and its result.
Think about Github or your bank account. There you have a history of every single change. This concept helps to guarantee consistency, especially for money-related applications.
This approach is overwhelming for such a simple and small application. However, this application was created to demonstrate what is possible in TypeScript and NestJS.
Please read my article if you want to learn more about DDD, CQRS, and Event-Sourcing. You will find links to these articles at the bottom.
Open Bank Account Flowchart
The flowchart illustrates the POST request for opening a bank account. You’ll find a detailed explanation below the flowchart.
POST /api/v1/bank-account/open
Firstly, the API Gateway will pick up the HTTP request, which forwards this request as gRPC protocol to the account microservice. Each microservice is split into a Command and a Query application. Since we will make a write request, this request will land on the Command application.
There, the OpenAccountDto validates the incoming request. A command OpenAccountCommand will be created and handled by the Command Bus. The executed command will create the aggregate, which could be seen as an event container. Many things could happen. For example, we could replay previous events of this aggregate.
However, this isn’t necessary since an account can be opened only once. Instead, we only create an AccountOpenedEvent and save this event and its event data in the MongoDB database.
Then, we produce a message that holds the event data and sends this message over the Kafka Event Stream to the query application, where a consumer listens to incoming calls.
We publish the event OpenedAccountEvent, which we received from the command application to the Event Bus. This event will then create a new entry in the PostgreSQL database in its execution. The eventual consistency is archived since write and read databases are in sync at this level.
In the meantime, the OpenAccountSaga sends a gRPC request to the second microservice bank-funds-svc to deposit the initial opening balance. Again, this is a write request, so the command application of the bank-funds-svc will pick up this request.
From there, the procedure is pretty similar to the original request. Commands will be created and executed, aggregate calls the event, create an entry to the read and write database, etc.
Code Snippets
To better understand the workarounds, I would like to share some code snippets with you. Don’t forget that this project is open-source. You’ll find the repositories below.
Controller
Command Handler
Event Handler
Repositories
- Bank Account Microservice
- Bank Funds Microservice
- API Gateway
- Event Sourcing Module
- Shared Repository for Proto files
Thanks for reading my article about my open-source project written in TypeScript and Go. I hope you can take something away. Please leave some feedback and follow me for more.
Cheers!
Read Next
Microservices with CQRS and Event Sourcing in TypeScript with NestJS was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Kevin Vogel
Kevin Vogel | Sciencx (2022-05-02T00:09:08+00:00) Microservices with CQRS and Event Sourcing in TypeScript with NestJS. Retrieved from https://www.scien.cx/2022/05/02/microservices-with-cqrs-and-event-sourcing-in-typescript-with-nestjs/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.