Simple Bank Sample using Dolittle
This sample shows a simple bank sample and how you could build it using Dolittle. Documentation for Dolittle can be found here.
You need to have the following installed:
The sample requires the 2.2 version of the .NET components. An upgrade in Dolittle is underway to support the latest version of 3.1 for all.
To see if you have the correct runtime installed, you should do the following from a terminal:
$ dotnet --list-runtimes
It should show a list similar to the following:
Microsoft.AspNetCore.All 2.2.8 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.All] Microsoft.AspNetCore.App 2.2.8 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 3.0.1 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 3.1.0 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.NETCore.App 2.1.14 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] Microsoft.NETCore.App 2.2.8 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] Microsoft.NETCore.App 3.0.1 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] Microsoft.NETCore.App 3.1.0 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
If it does not have a Microsoft.AspNetCore.All/App or a NETCore.App og 2.2.x, you should go and download and install it from here. This will not replace your 3.x tooling, but install the missing runtime components.
Note: There are currently some issues with dependencies while using
npm. So for the time being, please use
yarninstead see here.
The application consists of 2 microservices; Banking and Glance. They are configured as follows:
For the read models, you will be needing a Mongo, the easiest way through Docker is to run it as a daemon for your self:
$ docker run -d -p 27017:27017 mongo:4.0.13
This sample utilizes MongoDB as an event store as well.
Note: Due to the introduction of a breaking change in MongoDB that Dolittle was relying on, we are very specific on the version for now till we have implemented a fix.
Starting the Microservices
Run them accordingly using multiple shell windows:
dotnet runin the
Corefolder of both - this will run the backends
yarn startin the
The frontend projects are leveraging the WebPack DevServer
and utilizes its proxy capabilities for the necessary routes to the backend without having to
leave the same origin. The path
/swagger is proxied and will go to the backend.
So for it to do commands and queries, it can do so without changing origin.
This means that you can access the swagger interface even through the frontends.
Trying things out
The solution is a simple banking experience and to get started with the bank, we want to create a debit account and deposit some money to it. We're going to do this only using the backend code.
Both microservices are configured with the Dolittle Swagger debugging/development helper.
http://localhost:8080/swagger if you want to go through the frontend).
Opening a debit account
To open a debit account, open the command request called
Accounts/OpenDebitAccount and add the
required information. The CustomerID should be a Guid and represents
the unique identifier for the customer that is opening the debit account.
You could use this Guid:
Execute you should see a response as below, this is what we call the CommandResult.
It contains information about the result after performing a CommandRequest. Typically, if there are
any authorization, validation or business rules broken - they'll show up in this. Also, if there for
some reason is an exception; typically due to infrastructural issues - this will also be here.
With Dolittle being focused around CQRS and Event Sourcing, there is no unique key that come from the underlying data store. To overcome this problem, we use Guid as primary keys in everything. These can be generated already in the frontend if we want to. In our sample, this is done in the command handler.
The operation of opening the debit account is a command, and is represented by a type found here. In order for it to be handled, it relies on it being valid - which is where the input validator for the command fits in, which is found here. Once its valid, we go on to the actual handling of the command, which is implemented here.
The command handler will then coordinate the effort of what it means by opening a debit account. It relies on what is called an Aggregate Root to do this. Our particular aggregate root for this scenario is called DebitAccounts. It governs all the accounts for a customer and all the rules associated with organizing accounts. One of the rules for instance is that you are not allowed to have 2 accounts with the same name. Running the same command with the same properties on it, should therefor yield the following result:
The event that gets produced from this is called DebitAccountOpened. From this event we typically keep an optimized read model for each feature that is interested in the event that is produced. Each feature has an opportunity to optimize this read model in any way they want and use any storage technology - or even multiple storage technologies for the same feature for different purposes. This is possible due to the fact that the source of truth will be in the event store. In fact, we can consider the read model a perfect cache, a current state.
The event gets processed by an event processor, and the one dealing with the
sits here. What it does is basically create an instance
of a read model with the unique identifier;
EventSourceId that comes as the metadata associated with
the event. Then it inserts this into the database; in our case a MongoDb instance.
Depositing money to our debit account
In order for us to deposit money to the debit account, we need to go find the identifier of the account created. Navigate to the Queries spec in the swagger UI:
Then execute the
The query itself is implemented here. From the response, we're interested in the Guid of the debit account. Copy it from the response:
With the Guid of the debit account, we can now deposit money to it. Navigate back to the Commands spec in the swagger UI:
Open up the
Accounts/DepositMoneyToDebitAccount command and insert the Guid
into the Account property. Then put an amount in and execute it.
The command can be found here,
its input validation is here
and handled by this command handler.
Notice that it works with a different aggregate root; DebitAccount;
singular. This aggregate will be responsible for all things important to an account, and governs
all the rules for a single debit account.
Going back to the Queries spec and running the
AllAccounts query should now
yield a result with the amount you deposited to the account.
Event Horizon and events between microservices
In Glance there is a second event project called
This holds the events Glance is interested in from Banking.
Notice that the event is tagged with an attribute of
the identifier of the event found in
artifacts.json in Banking
.dolittle folder of the
This same identifier is also configured in the
file of Glance in the
.dolittle folder of the
Core project there.
There is an
EventProcessor in Glance that consumes this event and
prints out a message.