Building Event-driven Microservices Using CQRS and Serverless

Tuesday, January 31, 2017

This blog series will introduce you to building event-driven microservices as cloud-native applications.

In this first post, we’ll explore how to implement the CQRS pattern in microservices. We’ll also dive into why serverless is a natural fit for these kinds of systems. Later in the series we’ll explore a reference application that uses Spring Cloud Stream to implement CQRS.

What is an event-driven architecture?

Event-driven architectures treat domain events as first-class citizens. This approach is as old as software itself.

One example we use every day is in front-end applications. In every web browser in use today, events are handled as a way to capture inputs of a user form. Events connected to page elements are handled by an explicitly mapped function, sometimes referred to as an action or command, which will apply state changes to a user interface when triggered.

Now, more recently, with the widespread adoption of microservices, there is renewed interest in how to take advantage of event-driven techniques in distributed back-end systems.

CQRS

One of the most popular practices in event-driven architectures today is called CQRS, which is short for Command Query Responsibility Segregation. CQRS is a style of architecture that allows you to use different models to update and read domain data.

CQRS model

The basic idea of CQRS is that it’s perfectly natural to need to separate the models you’re using to update and read data. The diagram above shows this basic idea.

CQRS is popular for event-driven architectures because domain events — as inputs — are structurally different than the domain model they are subject to. Take for example the following domain model object representing an account.

Example 1. Account aggregate
{
  "createdAt": 1481351048967,
  "lastModified": 1481351049385,
  "userId": 1,
  "accountNumber": "123456",
  "defaultAccount": true,
  "status": "ACCOUNT_ACTIVE"
}

When a service wants to query for an account, this is the model it will expect. Now, what if we wanted to update the status to ACCOUNT_SUSPENDED? Normally this would be a simple update to the domain object for the status field. Now, what if we wanted to use a domain event to update the status instead? Since a domain object is structurally different from an event, we will need an API that accepts a different model as a command.

The following snippet is a domain event that transitions the state of the account from ACCOUNT_ACTIVE to ACCOUNT_SUSPENDED.

Example 2. Account event
{
  "createdAt": 1481353397395,
  "lastModified": 1481353397395,
  "type": "ACCOUNT_SUSPENDED",
  "accountNumber": "123456"
}

To process this domain event and apply the update to the query model, we must have an API to accept the command. The command will contain the model of the domain event and use it to process the update to the account’s query model.

CQRS suspend account

This is the simplest explanation of CQRS — to separate the command model from the query model. The complexity we often see today is more to do with the flavor of implementation. This is especially true when applying the pattern to microservices.

CQRS and Microservices

When CQRS combines with microservices, things get a bit complex — to say the least. Let’s take a look at what a "simple" microservice resembles that implements CQRS using Spring Boot.

CQRS microservice architecture

The diagram above is a rough sketch of an implementation of the CQRS pattern.

Here we’ve split up a single microservice into a command-side, query-side, and event processor—all of which can be deployed independently of one another.

Command-side

The command-side in this example exposes a REST API that accepts requests over HTTP. Requests take the form of commands that drive state changes to domain data owned by the microservice. To put it simply, any writes on domain data will flow from an API request as a command — which handles an action that results in changes to the database.

Command-side CQRS

Commands trigger actions and actions trigger domain events. The domain events persist to an event store—a fancy way to say "a system that combines a database together with a message broker."

An excellent event store to get started with is called Eventuate, which was founded by Chris Richardson as a project to help apply CQRS and Event Sourcing to microservices.

Domain events store as a series of time-ordered events appended to a log. Since every command generates an event, we’re able to rebuild the total state of the current system from a history of collected events. I cover this topic in more detail in a previous blog post on event sourcing in microservices.

Event Processor

The next component we’ll examine is the Event Processor. This CQRS component takes the form of a worker application that is responsible for ingesting domain events. The event processor is stateless and listens for messages from the event store, applying an action for incoming event messages.

Event processor CQRS

The event processor can respond to a new domain event in many useful ways. One domain event can spawn more events that can be sent to other microservices. This is why most developers of microservices are attracted to CQRS—as a way to publish and subscribe to domain events originating from applications outside of a bounded context.

This approach provides us with a mechanism to ensure referential integrity of domain data. Messages from other microservices can be used to handle domain events that allow maintenance of pesky foreign key relationships relating domain data from other records in the distributed system.

Query-side

The event processor is first and foremost responsible for applying a domain event that changes the state of a domain aggregate.

Each domain event can be used to update a database record that results in an incremental materialized view for describing an aggregate. In turn, the query-side will expose a REST API that allows HTTP clients to read the resulting materialized views that were generated from the processed events.

Query-side CQRS

The constraint in the query-side component is that domain data is read-only. All state changes in this system will flow in from the command-side and result in materialized views that can be read on the query-side.

Distributed Monolith or Microservice?

Now if you’re like me, you might be thinking "Hold up! That’s no moon…​ it’s a space station."

When most people think of a single microservice today, they think of an independent service component. In most cases, a microservice is built as an application that focuses on doing one thing well. Most importantly, the service can be upgraded and deployed independently of other services.

Now when it comes to the conventional CQRS implementation, because of the separate components, there seems to be a slight concern for calling it a microservice. So it’s worth asking: is this CQRS application considered a microservice? Or rather, could it be what some developers have started to refer to as a distributed monolith?

The answer is tricky, and it depends on who you’re asking. I find that a microservice is all about empowering small independent teams to continuously deliver features as a part of a larger ecosystem of other microservices.

CQRS deployments are complex when compared to most microservice deployments. For a microservices team, the goal is to be able to continuously deliver features into production. Since the separated components of CQRS can still be independently deployed, then we can say that each unit of deployment still satisfies the minimum requirements for independently delivering features into production. One feature of a microservice should always require—at most—one deployable unit.

A distributed monolith happens when a feature is delivered that requires a coordinated deployment of multiple separate components at the same time.

Serverless

Event-driven architectures need not apply only to microservices.

Serverless—which is also referred to as FaaS (Function-as-a-Service)—allows you to deploy code as functions without needing to setup or manage application servers or containers. Serverless is a popular architectural style that is rapidly gaining traction when building and operating cloud-native applications. One notable benefit of using serverless functions is that the concept of events is treated as a first-class citizen.

Microservices and Serverless

A popular misconception of cloud-native applications is that microservices and serverless are largely incompatible, incongruent, or orthogonal styles of architectures.

Let’s consider the CQRS system that we reviewed earlier. The rule of thumb still holds for applications built as serverless functions.

A distributed monolith happens when a feature is delivered that requires a coordinated deployment of multiple separate components.

The inverse of this rule would imply that a microservice can also be built from a composition of serverless functions. In this scenario, where would one microservice begin and another end?

Serverless microservice

One way to look at it is to consider the boundary of the microservice to be the boundary of the team. As long as a team can both independently and continuously deploy features as a function, then the boundary of the microservice is just the subset of functions (or application components) responsible for powering the features owned by the team.

Trade-offs

To understand why you would want to take this approach requires a close examination of the trade-offs.

Velocity

For microservices, the goal is velocity. We can measure velocity by answering the following two questions.

  • How fast can a developer make a single line of code change and safely deploy it into production?

  • How fast can a new developer ramp up and safely make changes to a code base?

A microservice team measures their velocity by answering the two questions. The lower the average time, the higher the velocity a team will have when delivering features.

Serverless comes with a learning curve, but lends well to increasing velocity in microservices. It does so by moving much of the workflow management out of the core components and into small composable functions that can be independently upgraded and deployed. This minimizes the time it takes for a developer to understand how a single function works and how to safely change it.

Serverless functions are also simple to upgrade and deploy, but may add complexity in understanding the whole picture. Managing hundreds of serverless functions as one team does not sound like much fun.

Complexity

Complexity is unavoidable in software. Complexity grows over time as a code base ages. Monolithic applications become unwieldy and hard to change when complexity grows, or when frameworks or languages become out of date or obsolete. Microservices split up this complexity into a distributed system, where each deployable unit is both easy to understand and easy to change by a small agile group of developers.

Cloud-native CQRS

This blog post is the first in a series that will introduce you to a reference application for building a cloud-native CQRS application as a collection of event-driven microservices and serverless functions.

Cloud-native CQRS with serverless

In the next blog post, we will dive into an open source sample application that has a similar structure to the diagram shown above. We’ll explore how to implement a variant of CQRS that is composed of multiple independently deployable cloud-native components.

The sample will demonstrate how to build an end-to-end microservice application that is flexible to change and more easily decomposed. We’ll explore using Spring Boot together with AWS Lambda to apply the CQRS pattern to a cloud-native application.

No comments :

Post a Comment

Be curious, I dare you.