Professional Documents
Culture Documents
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.
@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.
@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.
For our purpose, we will create the following DTO model classes.
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.
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.
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.
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.
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.
After triggering the end-point, we visit the H2 console. Now there should be two
more events. The MoneyCreditedEvent and then, the AccountActivatedEvent.
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.