24
CQRS: An Architecture of a Bounded Context

WHAT’S IN THIS CHAPTER?

  • The challenges of using a single model for complex presentation and domain logic needs
  • How the CQRS pattern segregates the reading and writing models
  • The popular misconceptions of the CQRS pattern
  • How CQRS can help bounded contexts to scale

Wrox.com Code Downloads for this Chapter

The wrox.com code downloads for this chapter are found at www.wrox.com/go/domaindrivendesign on the Download Code tab. The code is in the Chapter 24 download and individually named according to the names throughout the chapter.

CQRS (Command Query Responsibility Segregation) is a simple pattern that you can apply to a bounded context. It separates the domain model into two models: a read model and a write model (sometimes called a transactional model).

The reason for the separation is to enable a model to serve the needs of a single context without compromise. The two contexts in question are reporting on the state of the domain and performing business tasks, also known as the read and write sides. Using a single model for bounded contexts that have complex presentation needs and rich domain logic often results in that model becoming overly complex and devoid of integrity, generating confusion for domain experts and a maintenance nightmare for developers. By applying the CQRS pattern, a model is split in two, enabling each model to be optimized to serve each context more effectively.

CQRS is not a top-level architecture; it is a pattern for handling complexity that can be applied against bounded contexts needing to support a presentation model that is not aligned to the structure of the transactional model. The majority of web applications see a disparity between queries and commands. CQRS splits these two and enables the sides to be optimized without compromise.

This chapter introduces you to the CQRS pattern, giving you a holistic understanding of where it can be effective. Chapters 25, “Commands: Application Service Patterns for Processing Business Use Cases,” and Chapter 26, “Queries: Domain Reporting,” go deeper into the implementation of the command and query sides of the architecture.

The Challenges of Maintaining a Single Model for Two Contexts

Figure 24.1 shows a typical layered architecture of a bounded context. Within the heart of this architecture lies a domain model. The domain model is created to enforce the invariants of the domain when handling transactional operations. The model is composed of small aggregate groupings of domain objects built for consistency and that express the rules and logic of the domain. The reporting needs of an application, however, may not be aligned with the structure of the aggregates, resulting in application services needing to Load many different aggregates to construct view models that may contain only a subset of the data that is retrieved within the aggregate instances. This view generation can quickly become complex and difficult to maintain and in the very worst scenarios can slow down the system.

images

FIGURE 24.1 A single model fulfilling the read and write sides of an application.

To support view generation, domain models need to expose internal state and need to be adorned with presentation properties that have little to do with the invariants of the domain. Repositories often contain many extra methods on the contract to support presentation needs such as paging, querying, and free text searching. Because the read side of an application is typically used more frequently than the write side, users often seek improvements in report generation. To try to simplify as well as improve performance of the query side, the model is compromised. Aggregates are merged, and lazy loading is used to prevent pulling data that is not required for transactional business task processing needs, but that is required for presentational purposes. This leads to a single model that is full of compromises and is sub-standard for both reading and writing.

A Better Architecture for Complex Bounded Contexts

Figure 24.2 demonstrates an architecture that employs the CQRS pattern. It treats the needs of the two conflicting contexts—namely, reads and writes—separately by providing two models instead of one. Each model can now be optimized for the specific context it serves while retaining its conceptual integrity. You are in a sense applying the bounded context pattern at a lower level by binding one model for the read context and a separate model for the write context.

images

FIGURE 24.2 The CQRS pattern with a separate read and write model.

Figure 24.2 shows the segregation between commands; the responsibility to fulfill business tasks, which are invoked by a client, and queries and the responsibility to fulfill reports, which are requested by a client. In Figure 24.2, the same data store is used for both the read and the write side of the architecture. This is not mandatory; a separate read store can be employed to scale the read side.

The Command Side: Business Tasks

The command side of the architecture is concerned with upholding the rules of the domain. It represents the domain logic that satisfies business tasks. The architecture shown in Figure 24.3 at first glance is the same as the typical layered approach; however, the command side does not support querying, and any responses are merely acknowledgements on the success of the business task a client initiates.

images

FIGURE 24.3 The command side of CQRS.

Explicitly Modeling Intent

A command is a business task, a use case of a system, and it lives within the application layer. You should write commands in the language of the business. This is not UL; it is the language that captures the behaviors of the systems rather than the terms and concepts of the domain model. Typically, if you are following a BDD approach, commands come from the use cases and stories you produce.

You should model commands as verbs rather than nouns. They should capture the intent of the user explicitly. An example of a command is shown in Listing 24-1. A command is a simple data transfer object (DTO) with simple parameter validation.

As you can see from Listing 24-1, the command name reveals the intent of a user. In this case, it is for a customer to redeem a gift certificate. The command represents a request for a business task to be actioned and is therefore written in the present tense (for example: I want to do something) as opposed to domain events, which are written in past tense (for example: Something happened).

A Model Free from Presentational Distractions

A model that serves both the presentational and the transitional needs of an application often resembles the user interfaces of that application. The shape of aggregates is morphed from handling invariants into structures that match the user interface. For example, take the mockup of a user interface in Figure 24.4. This dashboard-like screen presents various attributes of a customer.

images

FIGURE 24.4 A user interface pulling data from many aggregates.

Listing 24-2 shows the type of domain object that is often created to meet the needs of the presentation while also implementing the logic of the domain.

A view is now easy to generate for the presentation model because the domain model is in complete alignment with the user interface. It is simply a case of retrieving the full customer aggregate and mapping it to a view model, as demonstrated in Listing 24-3.

However, the aggregate is now very large and is responsible for anything associated with a customer. The aggregate is structured around the UI rather than the invariants of the domain. In other words, the aggregates are based on a report screen instead of domain behavior. You can avoid these issues by applying CQRS and freeing the model from any presentational requirements. In the command model, there is no benefit in creating a single concept of a customer, often seen as a code smell and referred to as the god object. Instead, there will be an aggregate responsible for the rules governing loyalty, a separate aggregate for customer details, and a third for gift certificate balance. The UL will fit better around the behaviors of application than the UI screen. Domain experts will talk about behavior and rules, not about UI.

Without its added responsibilities the command model can be smaller and more focused on behavior with application services, and aggregates can become more concise. Repositories can be massively simplified as aggregate retrieval is restricted by ID rather than a host of querying methods such as paging and sorting. Developers can model aggregates around invariants and focus on transactional behavior without the noise of the presentation needs.

Handling a Business Request

A command handler is a flavor of an application service. The handler processes the command and contains logic to orchestrate the completion of a task. This logic can include delegation to the domain model, persistence and retrieval, and calling out to infrastructure services such as e-mail clients of payment gateways.

A command handler only returns an acknowledgement of the success or failure of a command; you should not use it to query or report in the domain state. Listing 24-4 presents an example of a command handler. Details of how to implement the command handler pattern are covered in the next chapter.

Because a domain model on the command side is built to implement domain rules and logic, it does not need to contain unnecessary presentational properties. DDD aggregates support command processing rather than model real life. Handlers can help to focus aggregates on behavior and invariants rather than on real life. Specific commands don’t need full domain entities when you’re handling them. (That is, they don’t need the customer name when you’re performing some action on the customer aggregate.) Customer entity doesn’t make sense. This helps keep aggregates small. This again aids you in modeling smaller aggregates with fewer associations.

The Query Side: Domain Reporting

The architecture of the query side, as shown in Figure 24.4, is concerned with reporting on the domain. The objects returned from the query side are simple DTO view models tailored to the specific needs of the view. The domain model for the command side is not required, because a view can be generated directly from the data store. The query side does not need to create an abstraction over the persistence store, so a repository in this context makes no sense; a persistence framework or light-weight libraries like ADO.NET should be used here.

Because the read model is still within the domain layer, it is able to use domain objects from the read side to perform calculations if this data is not precalculated within the data store. Typically, specification classes have been employed to provide an answer to view model properties based on some data pulled back from the database.

Reports Mapped Directly to the Data Model

The read side of the architecture maps report requests modeled as view models directly to the data model, bypassing the command model completely. Views can be built within the data model for each UI screen or report. This results in presummarized views, which are fast to retrieve, simple when mapping the raw data to view models, and able to handle paging and sorting and free text searches.

If the command side does not pre-computer a required value, use a specification or domain service to calculate the value on the fly as shown in Listing 24-5.

You can use a micro Object Relational Mapper (ORM) in the query service—something lightweight that can quickly map raw data to a DTO view model. The read side will be simple—devoid of any logic save that of pulling data and mapping to DTOs and delegating to specifications or domain services for decisions based on saved state.

Materialized Views Built from Domain Events

You can go further still with the segregation of read and write by using a different data schema. You can build a read model from domain events raised from commands; you can then use these events to build materialized views, as shown in Figure 24.6.

images

FIGURE 24.5 The query side of CQRS.

images

FIGURE 24.6 Using a different data store for querying.

A read data model can be denormalized and optimized for querying, including precalculated data.

Listing 24-6 shows how an action on the command model leads to a domain event being raised and then persisted by updating the read model. This can happen within the same transaction and the same database. Later you will see how updating the read model out of process can enable your application to scale.

The Misconceptions of CQRS

There are many misconceptions about the pattern of CQRS, but as you have read, it is simple, and at its core is the case of using a specific model for a specific context. If you have read anything of CQRS online, you may be thinking that you need to use heavyweight messaging frameworks, or your read store needs to be eventually consistent. This is not the case, although as you will read later you can employ these techniques to extend the CQRS pattern to scale your application when required. This section lists many of the popular misconceptions of the CQRS pattern that you may have heard.

CQRS Is Hard

If you have read this chapter from the beginning, you should be comfortable with CQRS, and you should have realized that it is a simple pattern. At a fundamental level, it’s an implementation of the Single Responsibility Principle (SRP) applied at the domain model layer. It’s useful for solving the complexity that arises when a presentational model is not in alignment with a transactional model. CQRS does not prescribe frameworks, multiple databases, or design patterns. It only states that the two contexts should be handled separately for better effectiveness. It’s a conceptual mind shift rather than a collection of complex patterns and principles that you need to adopt.

CQRS Is Eventually Consistent

Eventual consistency is the practice of having a read model updated out of process and asynchronously to the update of the transactional model. This is not a prerequisite of CQRS, but it is often used to enable the read side of a model to scale. Eventually consistent read models add an extra layer of complexity to an application as users who check to see the result of their actions may be surprised to see an outdated screen. CQRS does not require you to be eventually consistent. You can use the same database and transaction to update the read model schema. In fact, your application’s read store may already be eventually consistent if you are heavily using caching. If you are adopting the CQRS pattern due to complexities resulting in the misalignment between presentation and transaction concerns, try starting off being immediately consistent, and only move to eventually consistent if you have performance issues. There is an overhead that you will learn about later in this chapter through utilizing eventually consistent read stores.

Your Models Need to Be Event Sourced

As covered in Chapter 23, using event sourcing is an effective method to build both the read and the write models; however, there is no prerequisite to using event sourcing or in fact domain events with CQRS. Event sourcing is a solution to a problem of ensuring that your audit trail is accurate, but it does make building the read model easier because you can create whatever projections you want from the historical event data.

Commands Should Be Asynchronous

CQRS does not insist on commands being sent in a fire-and-forget fashion. For highly collaborative domains, in which multiple users are making changes to the same data, asynchronous commands make sense. This enables them to be handled in turn and allows the application to scale and not be overwhelmed with load. However, commands that don’t return an acknowledgement regarding the success or failure require other ways to update the user to the success of an action. This could be via e-mail or extra behavior that handles failed messages. In the case of purchasing, this could be sourcing a substitution for the customer as opposed to simply failing an order outright.

CQRS Only Works with Messaging Systems

If you are looking to apply an eventually consistent read store or process commands asynchronously, then using a messaging framework is probably a good idea. However, if you are not, then adding a messaging system to your application is just needless complexity.

You Need to Use Domain Events with CQRS

Using events to build a materialized read model is an effective method to keep your read and write models separate; however, it is not critical, and you can use other methods of creating a materialized read store. As you have seen in Chapter 21, “Repositories,” aggregates can reveal a state by using the memento pattern. You can also use some of the patterns in Chapter 26 to provide presentation information directly from your domain objects in a nonobstructive manner. Finally, you can build views based on the relational data model of the write model.

Patterns to Enable Your Application to Scale

CQRS enables applications to perform well under heavy load. This is accomplished by the read and write side of an application being split. Separating the sides enables each to be scaled independently to meet the particular demands of the application. Read and write data can also be separated into stores that are best suited to their needs. A write store that deals solely with aggregates can utilize a document database or a key value store. A read model can utilize a relational database or a caching store.

However, to scale either side, you need to understand the trade-offs involved. Scaling out a system isn’t simply a technical decision. It is vital that the business understands that changes to the architecture of a system result in changes to the user experience, and such changes need to be handled carefully and be acceptable to the business. The CAP theorem discussed in Chapter 11, “Introduction to Bounded Context Integration,” states you can have two of the following: consistency, availability, and partition tolerance. By sacrificing immediate consistency and moving to a more eventually consistent read side, you are able to scale applications that have high demands on the report from the system. For domains that are highly collaborative, you can sacrifice the availability guarantee, by not handling a request immediately. This involves messages sent to the system being queued and handled out of process by other means, such as e-mail, or a status report signifying to the client the success of the request.

The most important point to take way is that any trade-offs made to scale your application affect user experience, and this affects your business. Therefore, it is important that the business make decisions that affect user experience. This section examines ways to scale both the read and the write side using the CQRS pattern, along with the trade-offs that you need to consider.

Scaling the Read Side: An Eventually Consistent Read Model

If the demands of your application are far greater on the read side than on the write side, having an eventually consistent read store can allow you to increase the availability and performance of your application. You can store a read model in a separate database from your write side, or you can replicate it to multiple databases or any persistence store. The way you store your data on the read side can be completely different from the way you store the state of your domain on the write side. However, to do so you must publish state changes from the write side to the read side so that they can be denormalized and stored specifically for query retrieval. Figure 24.7 shows that there is a queue between the read and the write side of the architecture. This queue contains a domain event that is raised upon a state change within the domain model. The read side processes that domain event and updates its read store. Your read side becomes eventually consistent as it grows slightly out of sync with the write side.

images

FIGURE 24.7 An eventually-consistent read model.

The Impact to the User Experience

An eventually consistent read store affects your user experience. How up-to-date your reporting data (UI display and traditional reporting) needs to be is a question for your business users. It is worth being explicit about the staleness of the data so users can consider it when making decisions on the data they have in front of them.

Use the Read Model to Consolidate Many Bounded Contexts

You can use a read model to consolidate views from across your enterprise to simplify report rendering. Other bounded contexts that expose reports on the state of their domains via Restful URLs or messaging systems can be consumed and used within the read store to consolidate the data required to satisfy a composite UI. Figure 24.8 shows how such a system might look.

images

FIGURE 24.8 Consolidate data from many bounded contexts into a single read model.

Using a Reporting Database or a Caching Layer

You may already be using a copy of your transactional database as a reporting database that is replicated through log shipping. This is a form of separating your read and write concerns. If it is applicable, you can use this simple method to scale your read side, as shown in Figure 24.9.

images

FIGURE 24.9 Use a copy of the transactional database for the read model.

Scaling the Write Side: Using Asynchronous Commands

If you have a highly collaborative domain with many users making changes to the same set of entities, handling business tasks out of process enables you to scale your application to handle the high load. Figure 24.10 shoes how you can use a message queue to store requests for business tasks. The application layer accepts the request from a client to perform a business task, but instead of executing the request straight away, it handles it out of process. The application layer can only acknowledge that the request was received; the client must be notified in a different manner through an e-mail or simply by checking on the status of the request.

images

FIGURE 24.10 An asynchronous write side.

Command Validation

If you are sending requests for business tasks asynchronously, you need to be sure, as far as you can, that they will succeed because you have no way of receiving immediate acknowledgement to the success or failure of the request. The application service should perform some basic validation of the request and perhaps even use the view store to check invariants before adding the request to the queue for processing later. An application should do enough validation work to ensure that if a request fails, it is because of business reasons and not because of a missing or incorrect parameter.

Impact to the User Experience

To minimize confusion for users, make it clear that the execution of a request is handled out of process, and an acknowledgement merely confirms that a request was accepted, not that it has succeeded. In some domains, this kind of experience, such as placing an order on an e-commerce site, is normal to a user. As long as the user receives confirmation that a request for an order has been placed and that he has an order ID, he is happy. He is also aware that when it comes to processing the order, his card may fail or some items that were in stock at the time of ordering may now be out of stock and on order for replenishment. If, however, you are in a domain in which users expect to view the changes that their request was intended to make, you need to make it explicit that there will be a delay.

Scaling It All

If you are working in a collaborative domain with heavy reads and writes, you can use an eventually consistent read model in conjunction with business tasks being handled out of process. Figure 24.11 shows you how both sides would look if you needed to scale out the read and write sides of an application.

images

FIGURE 24.11 Scaling out the read and write sides of CQRS.

The Salient Points

  • CQRS is not an architectural pattern to apply to all bounded contexts of a system.
  • Use CQRS if a domain model cannot meet the needs of complex presentation and domain logic without compromise. Split the model into two specific models: one for the read context and one for the write context.
  • By using segregation, you can shape aggregates on the write side for behavior and design them around invariants and not for reporting needs.
  • View models tailored to reporting needs on the read side can bypass the domain model and pull data directly from the database, which can be tuned for better performance.
  • CQRS is a simple pattern. It’s a simple case of applying the Single Responsibility Principle at the model level, creating two models instead of one.
  • CQRS is often incorrectly thought of as the application of messaging, eventual consistency, domain events, and event sourcing. Although all the above can enhance a system and be useful in specific contexts, they are by no means essential to apply the CQRS pattern.
  • CQRS can enable you to scale if you have a high number of reads by allowing you to introduce eventual consistency through separating the read side data model from the write side data model.
  • If you are designing a system for a highly collaborative domain with many writes to the same sets of aggregates, you can introduce asynchronous request processing on the write side of the segregation.
  • There are trade-offs with scaling and introducing eventual consistency to either the write or the read models. This trade-off must be understood and accepted by the business, and it must be considered in terms of the user experience.
  • CQRS enables your system to have limitless scalability on both the read and the write side.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.47.218