You are on page 1of 6

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.

In 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.

You might also like