Implementing Event Sourcing With Axon and Spring Boot - Part 3
We finish creating our app by implementing a service layer, creating data transfer objects, and defining REST controllers.
Join the DZone community and get the full member experience.
Join For Freein the previous post , we came pretty far in implementing event sourcing with axon and spring boot. however, we still didn't have a proper way of testing our application. in this post, we will take care of that.
to do so, we would expose certain rest interfaces from our application. these interfaces will allow us to create an account and also perform other operations. basically, you can think of these rest interfaces as apis.
so without further ado, let's start implementing.
the service layer
as a first step, we will create a service layer. we will have two service interfaces. the irst is accountcommandservice
to handle the commands. the second is accountqueryservice
. at this point, the query service will just help in fetching a list of events.
basically we try to follow solid principles. therefore, we will code to interfaces. below are the interfaces declarations for our services.
public interface accountcommandservice {
public completablefuture<string> createaccount(accountcreatedto accountcreatedto);
public completablefuture<string> creditmoneytoaccount(string accountnumber, moneycreditdto moneycreditdto);
public completablefuture<string> debitmoneyfromaccount(string accountnumber, moneydebitdto moneydebitdto);
}
public interface accountqueryservice {
public list<object> listeventsforaccount(string accountnumber);
}
now, we implement these interfaces. the first is the accountcommandserviceimpl
.
@service
public class accountcommandserviceimpl implements accountcommandservice {
private final commandgateway commandgateway;
public accountcommandserviceimpl(commandgateway commandgateway) {
this.commandgateway = commandgateway;
}
@override
public completablefuture<string> createaccount(accountcreatedto accountcreatedto) {
return commandgateway.send(new createaccountcommand(uuid.randomuuid().tostring(), accountcreatedto.getstartingbalance(), accountcreatedto.getcurrency()));
}
@override
public completablefuture<string> creditmoneytoaccount(string accountnumber, moneycreditdto moneycreditdto) {
return commandgateway.send(new creditmoneycommand(accountnumber, moneycreditdto.getcreditamount(), moneycreditdto.getcurrency()));
}
@override
public completablefuture<string> debitmoneyfromaccount(string accountnumber, moneydebitdto moneydebitdto) {
return commandgateway.send(new debitmoneycommand(accountnumber, moneydebitdto.getdebitamount(), moneydebitdto.getcurrency()));
}
}
the main thing to note here is the commandgateway
. basically, this is a convenience interface provided by axon. in other words, you can use this interface to dispatch commands. when you wire up the commandgateway
as below, axon will actually provide the defaultcommandgateway
implementation.
then, using the send method on the commandgateway
, we can send a command and wait for the response.
in the example below, we basically dispatch three commands in three different methods.
then we implement the accountqueryserviceimpl
. this class is not mandatory for event sourcing using axon. however, we are implementing this for our testing purpose.
@service
public class accountqueryserviceimpl implements accountqueryservice {
private final eventstore eventstore;
public accountqueryserviceimpl(eventstore eventstore) {
this.eventstore = eventstore;
}
@override
public list<object> listeventsforaccount(string accountnumber) {
return eventstore.readevents(accountnumber).asstream().map( s -> s.getpayload()).collect(collectors.tolist());
}
}
notice that we wire up something called eventstore
. this is the axon event store. basically, eventstore
provides a method to read events for a particular aggregateid
. in other words, we call the readevents()
method with the aggregateid
(or account#) as input. then, we collect the output stream and transform it to a list. nothing too complex.
the data transfer objects
in the next step, we create data transfer objects. even though our resource might be the entire account, the different commands will require different payloads. therefore, dto objects are required.
for our purpose, we will create the following dto model classes.
accountcreatedto
is used for creating a new account.
public class accountcreatedto {
private double startingbalance;
private string currency;
public double getstartingbalance() {
return startingbalance;
}
public void setstartingbalance(double startingbalance) {
this.startingbalance = startingbalance;
}
public string getcurrency() {
return currency;
}
public void setcurrency(string currency) {
this.currency = currency;
}
}
then, moneycreditdto
for crediting money to an account.
public class moneycreditdto {
private double creditamount;
private string currency;
public double getcreditamount() {
return creditamount;
}
public void setcreditamount(double creditamount) {
this.creditamount = creditamount;
}
public string getcurrency() {
return currency;
}
public void setcurrency(string currency) {
this.currency = currency;
}
}
lastly, we have the moneydebitdto
for debiting money from an account.
public class moneydebitdto {
private double debitamount;
private string currency;
public double getdebitamount() {
return debitamount;
}
public void setdebitamount(double debitamount) {
this.debitamount = debitamount;
}
public string getcurrency() {
return currency;
}
public void setcurrency(string currency) {
this.currency = currency;
}
}
as you can see, these are just standard pojos. so as to enable jackson to be able to serialize and deserialize the objects we have declared standard getter and setter methods. of course, in a real business case, you might also have some validations here. however, for the purpose of this example, we will keep things simple.
defining the rest controllers
this is the final piece of the whole application. in order to allow a consumer to interact with our application, we need to expose some interfaces. spring web mvc provides excellent support for the same.
so as to keep things properly segregated, we define two controllers. the first one is to handle the commands.
@restcontroller
@requestmapping(value = "/bank-accounts")
@api(value = "account commands", description = "account commands related endpoints", tags = "account commands")
public class accountcommandcontroller {
private final accountcommandservice accountcommandservice;
public accountcommandcontroller(accountcommandservice accountcommandservice) {
this.accountcommandservice = accountcommandservice;
}
@postmapping
public completablefuture<string> createaccount(@requestbody accountcreatedto accountcreatedto){
return accountcommandservice.createaccount(accountcreatedto);
}
@putmapping(value = "/credits/{accountnumber}")
public completablefuture<string> creditmoneytoaccount(@pathvariable(value = "accountnumber") string accountnumber,
@requestbody moneycreditdto moneycreditdto){
return accountcommandservice.creditmoneytoaccount(accountnumber, moneycreditdto);
}
@putmapping(value = "/debits/{accountnumber}")
public completablefuture<string> debitmoneyfromaccount(@pathvariable(value = "accountnumber") string accountnumber,
@requestbody moneydebitdto moneydebitdto){
return accountcommandservice.debitmoneyfromaccount(accountnumber, moneydebitdto);
}
}
then, we create another controller to expose one end-point. basically, this endpoint will help us list the events on an aggregate.
@restcontroller
@requestmapping(value = "/bank-accounts")
@api(value = "account queries", description = "account query events endpoint", tags = "account queries")
public class accountquerycontroller {
private final accountqueryservice accountqueryservice;
public accountquerycontroller(accountqueryservice accountqueryservice) {
this.accountqueryservice = accountqueryservice;
}
@getmapping("/{accountnumber}/events")
public list<object> listeventsforaccount(@pathvariable(value = "accountnumber") string accountnumber){
return accountqueryservice.listeventsforaccount(accountnumber);
}
}
as you can see, these controllers mainly use the services we created earlier. the payload received is passed to the service that in turn uses the commandgateway
to trigger the commands. then, the response is mapped back to the output of the end-point.
as a last bit of help, we configure swagger for our application. basically, swagger provides a nice user interface that can be used to test our rest end-points. if you remember, we already added the swagger dependencies to our pom.xml file. now, we create a configuration class to configure swagger.
configuration
@enableswagger2
public class swaggerconfig {
@bean
public docket apidocket(){
return new docket(documentationtype.swagger_2)
.select()
.apis(requesthandlerselectors.basepackage("com.progressivecoder.es"))
.paths(pathselectors.any())
.build()
.apiinfo(getapiinfo());
}
private apiinfo getapiinfo(){
return new apiinfo(
"event sourcing using axon and spring boot",
"app to demonstrate event sourcing using axon and spring boot",
"1.0.0",
"terms of service",
new contact("saurabh dashora", "progressivecoder.com", "coder.progressive@gmail.com"),
"",
"",
collections.emptylist());
}
}
note that this is a very basic bare minimum configuration required for swagger. there are lot more options available that we will explore in some other post.
testing the application
finally, we are at the big moment. our application is complete. we can test it now.
run the application using the intellij maven plugin (since we are using intellij for our coding). the command clean package spring-boot:run
will do the trick.
once the application starts up, you can visit http://localhost:8080/swagger-ui.html .
step 1: creating the account
to start off, let's trigger post /bank-accounts from the swagger ui as below:
we fill the currency value and starting balance and click execute. then, we scroll below to see the response.
the response code is 200. this corresponds to http status ok. the response body has the account number of the account that was created. but how does the data look in the event store? we can easily check it out in the h2-console . in order to check the h2-console, we have to visit http://localhost:8080/h2-console .
we login to testdb. then, we can execute the below query in the console.
select payload_type , aggregate_identifier, sequence_number , payload from domain_event_entry
basically, if everything has gone fine, we should see some results. in our case, there will be two events created at this point. first is the accountcreatedevent
followed by the accountactivatedevent
. both events occur on the same instance of the aggregate as evident by the aggregate_identifier
. as you can see, the payload is encoded.
there are some other fields as well in the standard table created by axon. you can have a look. however, i have tried to fetch the most important ones.
step 2: debit money from the account
next we will try to debit some money from our account. we have already implemented an end-point to do so. we provide the account number created earlier as an input along with the dto.
after, triggering this transaction, we can visit the h2 console and run the same query as before.
we will see two more events on the same aggregate. the moneydebitedevent
and then the accountheldevent
. this is because we have setup our aggregate so that once the account balance goes below 0, the account is moved to hold status.
step 3: credit money to the account
now we will credit some money to the account. basically, we use swagger to trigger the credit end-point, i.e. /bank-accounts/credits/{accountnumber} . to improve our financial situation, we will add $100 usd.
after triggering the end-point, we visit the h2 console. now there should be two more events. the moneycreditedevent
and then, the accountactivatedevent
.
step 4: listing the events
lastly, we would also like to get a list of all the events on a particular aggregate or an account. after all, the whole point of event sourcing was to be able to read the events for some business purpose.
if you remember, we have already implemented an end-point to fetch the list of events. you can find it in the account queries section in swagger ui view for our application. basically, it is a get method.
we provide the account number as input and click execute.
if everything has gone fine till now, we will see the list of events in the output. while fetching, axon automatically decodes the payload to our usual data values.
conclusion
with this, we have successfully implemented event sourcing with axon and spring boot. the entire application code is available on github for reference.
there is a lot more tweaking that can be done on the application for production purposes. however, on a conceptual level, we have implemented everything required for building an event sourcing application.
in future posts, we will continue exploring event sourcing with another important data management pattern known as cqrs.
Published at DZone with permission of Saurabh Dashora. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments