Building an Event Driven Cycling Tracking Service in Go with CQRS
Introduction
In this post, we will be building a service that will enable users to track their cycling workouts on a map. We will explore the Command Query Responsibility Segration (CQRS) pattern along the way.
[INSERT PHOTO FROM STRAVA HERE]
Before we begin, lets actually define what we are going to be building. Our service should fulfill the following requirements:
- A user can create a new cycling session (a Ride).
- While a ride is active, a client can send positional updates to the service.
- While a ride is active, a user can pause the Ride, making it in-eligable for positional updates from a client.
- While a ride is paused or active, a user can end the Ride.
- After a ride has been finished, a user can view their Ride on a map.
We will be implementing this service in the Go programming language.
Getting Started
Let’s begin with the first requirement:
- A user can create a new cycling session (a Ride).
We will begin by creating a struct representing the entity that will be the core of our application: the Ride.
|
|
Each Ride will have a unique identifier, a state, and a list of events. The state will be used to determine when we can accept positional updates from a client. The events will be used to store the history of the Ride, including positional updates. We will use the events later to project the Ride onto a map for the user to view.
Let’s further define the possible states a Ride can be in:
|
|
These states are reflective of the requirements that we defined earlier.
Note the difference between ENDED
and FINISHED
. ENDED
is used to indicate that the ride has been completed by the user, but not fully processed by the system. One a Ride is FINISHED
, it is fully processed and can be viewed by the user on a map.
Finally, let’s define the RideEvent struct:
|
|
Our plan is to use events to track the positional updates that a client will send to our service. When a Ride is ended, we will use the RidePositionUpdated events on the Ride to project the Ride onto a map for the user to view. This will be handled by an asynchronous process that will be triggered by the RideEnded event, and will transition the Ride to the FINISHED
state once the analysis is complete.
Note that instead of using an EventCode type, we could have also just used a string. I personally prefer to use enums when possible, as they provide type safety and make the code more readable.
Access Patterns
Before we continue with the implementation of the service, let’s talk a little bit about how data will be accessed in our application. As our access pattern will dictate the overall design of our application, it is important to understand the tradeoffs that we are making.
Typically, many web services are read heavy applications. As the name implies, clients are more likely to read data from the service than to write data to it.
Our application however, is very write heavy. Depending on the length of a ride, we might be sending hundreds or even thousands of positional updates to the service throughout the lifecycle of a ride. If you extrapolate that over many users, our service might be responsible for recieving hundreds of thousands of positional updates per day.
Because of the write heavy nature of our application, it makes sense to explore ways of optimizing the write process, since that is the primary way that our data will be accessed.