Over the past three years I have been experimenting in building microservice applications with event sourcing and CQRS. My initial implementation was fairly successful, utilizing technologies like Elasticsearch, Kafka, RabbitMQ and Docker. With the rise in innovation of cloud computing, managed NoSQL databases, and serverless event buses I began to re-think how to better leverage these technologies within my application.
In this series of blog posts, I document my journey (including challenges) involved in building an event sourcing and CQRS microservices application in a fully cloud-native, serverless, and distributed nature hosted under AWS. The application is described further down below but before we get too deep in the details, let’s review the concepts and challenges we anticipate to face along this journey.
Update (Sept 14, 2021): I have migrated this app from AWS Amplify to AWS CDK as it was too complex to manage with Amplify. The repo for the project remains the same.
Traditional systems
Applications typically have to work with data in some way or another. The traditional approach is to persist the current state of data by updating the existing data.
With this approach it becomes incredibly difficult to understand how an aggregate changed to a certain state without first implementing some kind of audit trail mechanism (see my post about Memory Optimized Temporal Tables).
Another consideration is that a tradition database system automatically maintains ACID transactions. Working in a distributed system, especially one that is heavily event-driven introduces a lot of challenges that we will explore further on.
What is event sourcing?
Event Sourcing is a way of persisting application state by storing an immutable sequence of events. State changes are triggered to update application state as response to these events.
The current state of an application can always be determined by the aggregate of events.
The most relatable example for a software developer would be source version control. Each commit is an event representing state change — whether it is a file added, modified, or deleted. The aggregation of commits makes up the current state of the repository.
Why should I use event sourcing?
Event sourcing provides many advantages but not limited to:
- Audit trail. Events are immutable and provide a natural history of what has taken place in the system.
- Time travel. By persisting a stream of events, it is very easy to be able to determine the state of the system at any point in time by aggregating the events within the time period. This provides the ability to answer historical questions about the state of the system.
- Performance. Events are simple, immutable, standalone options that only require an append operation. The event store is typically optimized to handle high performance writes.
- Scalability. Storing events avoids the complications associated with saving complex domain aggregates to relational databases allowing more flexibility for scaling.
What is CQRS?
CQRS is a design pattern that separates operations which update data (commands) from the operations that read data (query). A command changes the state of an aggregate but does not return any data; a query returns data but does not alter the state of an aggregate.
A method should either change state of an aggregate, or return a result, but not both
This allows us to maintain the separation of concerns, SOLID principles, and optimize our data stores for reads and writes. Having such a model allows us to build a highly scalable and maintainable applications with sophisticated domain models.
Eventual Consistency
Ensuring data will be replicated from the event store to the read store of a CQRS application is vital to maintain accuracy. However it is impossible to guarantee exactly when this will happen due to factors like network latency.
Eventual consistency results in a substantially simpler system that is both higher in performance and easier to operate but there is a cost. Eric Brewer’s CAP theorem states that a system can only be consistent or available in the face of partitions but not both.
Having an eventually consistent system poses challenges in providing a real-time user experience on the front end. There are techniques for solving this and we will discuss them in a further post.
Distributed Transactions
Another challenge is a result of having distributed transactions. The entire concept of ACID transactions is no longer simple to uphold anymore. We can no longer rely on 2PC.
Instead we must now handle distributed transaction consistency through various orchestration techniques. We can make use of choreographed orchestration sagas to help address these challenges. See my post on DynamoDB Streams for an example of choreographing distributed transactions.
Pecuniary
The application I am building is called Pecuniary (adjective: consisting of or measured in money). It’s primary goal is to provide portfolio management and analytics (asset allocation, P/L, net worth, tax calculations, etc) for securities and related transactions in Canadian investment accounts. The event based nature of the application makes it an ideal candidate for an event sourcing and CQRS application.
Architecture
The command side is invoked every time something needs to change. The command services are also responsible for validating business rules and saving results in the form of events. Events are persisted to Amazon DynamoDB as a NoSQL event store.
The event bus is comprised up of serverless functions and topics that handle the asynchronous de-normalization of events into queryable data. These event handlers are what react to and update the de-normalized NoSQL read store used to support all the read operations.
Technology
The entire application is built using AWS Amplify. All the command and query actions are exposed via a GraphQL endpoint using AWS AppSync resolvers to each one of the event and read stores. The event notification bus will be handled using Amazon Simple Notification Service with an eventual goal to move towards AWS EventBridge.
All event handlers are deployed to AWS Lambda using NodeJS. The functions that process the financial calculations will be written in C# .NET Core for it’s computational efficiencies.
The front end is built using a modern JavaScript framework, React.js with Redux and Redux Thunk for state management. As we have already identified earlier, we will likely run into challenges building in a single page application (SPA) due to the asynchronous and event-driven nature of the application.
Summary
We have now seen what event sourcing and CQRS is and how it can provide a platform for event-driven microservice applications. We have also identified some of the challenges with this pattern and the techniques to address them. We have also seen the high level overview of the application and how it relates to event sourcing an CQRS.
I have no idea how long this journey will take nor how successful it will turn out. However I an excited for the opportunity to share with you all the successes and challenges along the way!
The posts on this blog are provided ‘as is’ with no warranties and confer no rights. The opinions expressed on this site are my own and do not necessarily represent those of my employer.