Compartilhar via


Journey 5: Preparing for the V1 Release

patterns & practices Developer Center

On this page: Download:
The Contoso Conference Management System V1 release | Patterns and concepts | Implementation details | Impact on testing | Summary

Download code samples

Download PDF

Order Paperback

Adding functionality and refactoring in preparation for the V1 release.

"Most people, after accomplishing something, use it over and over again like a gramophone record till it cracks, forgetting that the past is just the stuff with which to make more future." Freya Stark

The Contoso Conference Management System V1 release

This chapter describes the changes made by the team to prepare for the first production release of the Contoso Conference Management System. This work includes some refactoring and additions to the Orders and Registrations bounded context that the previous two chapters introduced, as well as a new Conference Management bounded context and a new Payments bounded context.

One of the key refactorings undertaken by the team during this phase of the journey was to introduce event sourcing into the Orders and Registrations bounded context.

One of the anticipated benefits from implementing the CQRS pattern is that it will help us manage change in a complex system. Having a V1 release during the CQRS journey will help the team evaluate how the CQRS pattern and event sourcing deliver these benefits when we move forward from the V1 release to the next production release of the system. The remaining chapters will describe what happens after the V1 release.

This chapter describes the user interface (UI) that the team added to the public website during this phase and includes a discussion of task-based UIs.

Working definitions for this chapter

This chapter uses a number of terms that we will define next. For more detail, and possible alternative definitions, see Chapter 4, "A CQRS and ES Deep Dive" in the Reference Guide.

Access code. When a business customer creates a new conference, the system generates a five-character access code and sends it by email to the business customer. The business customer can use his email address and the access code on the conference management website to retrieve the conference details from the system at a later date. The system uses access codes instead of passwords so that the business customer need not set up an account just to make a purchase.

Event sourcing. Event sourcing is a way of persisting and reloading the state of aggregates within the system. Whenever the state of an aggregate changes, the aggregate raises an event detailing the state change. The system then saves this event in an event store. The system can recreate the state of an aggregate by replaying all of the previously saved events associated with that aggregate instance. The event store becomes the book of record for the data stored by the system.

In addition, you can use event sourcing as a source of audit data, as a way to query historic state, gain new business insights from past data, and replay events for debugging and problem analysis.

Eventual consistency. Eventual consistency is a consistency model that does not guarantee immediate access to updated values. After an update to a data object, the storage system does not guarantee that subsequent accesses to that object will return the updated value. However, the storage system does guarantee that if no new updates are made to the object during a sufficiently long period of time, then eventually all accesses can be expected to return the last updated value.

User stories

The team implemented the user stories described below during this stage of the journey.

Ubiquitous language definitions

Business customer. The business customer represents the organization that is using the conference management system to run its conference.

Seat. A seat represents a space at a conference or access to a specific session at the conference such as a welcome reception, tutorial, or workshop.

Registrant. A registrant is a person who interacts with the system to place orders and make payments for those orders. A registrant also creates the registrations associated with an order.

Conference Management bounded context user stories

A business customer can create new conferences and manage them. After a business customer creates a new conference, he can access the details of the conference by using his email address and conference locator access code. The system generates the access code when the business customer creates the conference.

The business customer can specify the following information about a conference:

  • The name, description, and slug (part of the URL used to access the conference).
  • The start and end dates of the conference.
  • The different types and quotas of seats available at the conference.

Additionally, the business customer can control the visibility of the conference on the public website by either publishing or unpublishing the conference.

The business customer can use the conference management website to view a list of orders and attendees.

Ordering and Registration bounded context user stories

When a registrant creates an order, it may not be possible to fulfill the order completely. For example, a registrant may request five seats for the full conference, five seats for the welcome reception, and three seats for the preconference workshop. There may only be three seats available for the full conference and one seat for the welcome reception, but more than three seats available for the preconference workshop. The system displays this information to the registrant and gives her the opportunity to adjust the number of each type of seat in the order before continuing to the payment process.

After a registrant has selected the quantity of each seat type, the system calculates the total price for the order, and the registrant can then pay for those seats using an online payment service. Contoso does not handle payments on behalf of its customers; each business customer must have a mechanism for accepting payments through an online payment service. In a later stage of the project, Contoso will add support for business customers to integrate their invoicing systems with the conference management system. At some future time, Contoso may offer a service to collect payments on behalf of customers.

Note

In this version of the system, the actual payment is simulated.

After a registrant has purchased seats at a conference, she can assign attendees to those seats. The system stores the name and contact details for each attendee.

Architecture

Figure 1 illustrates the key architectural elements of the Contoso Conference Management System in the V1 release. The application consists of two websites and three bounded contexts. The infrastructure includes Microsoft Azure SQL Database (SQL Database) instances, an event store, and messaging infrastructure.

The table that follows Figure 1 lists all of the messages that the artifacts (aggregates, MVC controllers, read-model generators, and data access objects) shown in the diagram exchange with each other.

Note

For reasons of clarity, the handlers (such as the OrderCommandHandler class) that deliver the messages to the domain objects are not shown.

Follow link to expand image

Figure 1

Architecture of the V1 release

Element

Type

Sends

Receives

ConferenceController

MVC Controller

N/A

ConferenceDetails

OrderController

MVC Controller

AssignSeat
UnassignSeat

DraftOrder
OrderSeats
PricedOrder

RegistrationController

MVC Controller

RegisterToConference
AssignRegistrantDetails
InitiateThirdPartyProcessorPayment

DraftOrder
PricedOrder
SeatType

PaymentController

MVC Controller

CompleteThirdPartyProcessorPayment
CancelThirdPartyProcessorPayment

ThirdPartyProcessorPaymentDetails

Conference Management

CRUD Bounded Context

ConferenceCreated
ConferenceUpdated
ConferencePublished
ConferenceUnpublished
SeatCreated
SeatUpdated

OrderPlaced
OrderRegistrantAssigned
OrderTotalsCalculated
OrderPaymentConfirmed
SeatAssigned
SeatAssignmentUpdated
SeatUnassigned

Order

Aggregate

OrderPlaced
*OrderExpired
*OrderUpdated
*OrderPartiallyReserved
*OrderReservationCompleted
*OrderPaymentConfirmed
*OrderRegistrantAssigned

RegisterToConference
MarkSeatsAsReserved
RejectOrder
AssignRegistrantDetails
ConfirmOrderPayment

SeatsAvailability

Aggregate

SeatsReserved
*AvailableSeatsChanged
*SeatsReservationCommitted
*SeatsReservationCancelled

MakeSeatReservation
CancelSeatReservation
CommitSeatReservation
AddSeats
RemoveSeats

SeatAssignments

Aggregate

*SeatAssignmentsCreated
*SeatAssigned
*SeatUnassigned
*SeatAssignmentUpdated

AssignSeat
UnassignSeat

RegistrationProcessManager

Process manager

MakeSeatReservation
ExpireRegistrationProcess
MarkSeatsAsReserved
CancelSeatReservation
RejectOrder
CommitSeatReservation
ConfirmOrderPayment

OrderPlaced
PaymentCompleted
SeatsReserved
ExpireRegistrationProcess

OrderViewModelGenerator

Handler

DraftOrder

OrderPlaced
OrderUpdated
OrderPartiallyReserved
OrderReservationCompleted
OrderRegistrantAssigned

PricedOrderViewModelGenerator

Handler

N/A

SeatTypeName

ConferenceViewModelGenerator

Handler

Conference
AddSeats
RemoveSeats

ConferenceCreated
ConferenceUpdated
ConferencePublished
ConferenceUnpublished
**SeatCreated
**SeatUpdated

ThirdPartyProcessorPayment

Aggregate

PaymentCompleted
PaymentRejected
PaymentInitiated

InitiateThirdPartyProcessorPayment
CompleteThirdPartyProcessorPayment
CancelThirdPartyProcessorPayment

* These events are only used for persisting aggregate state using event sourcing.
** The ConferenceViewModelGenerator creates these commands from the SeatCreated and SeatUpdated events that it handles from the Conference Management bounded context.

The following list outlines the message naming conventions in the Contoso Conference Management System

  • All events use the past tense in the naming convention.
  • All commands use the imperative naming convention.
  • All DTOs are nouns.

The application is designed to deploy to Azure. At this stage in the journey, the application consists of two web roles that contain the ASP.NET MVC web applications and a worker role that contains the message handlers and domain objects. The application uses SQL Database instances for data storage, both on the write side and the read side. The Orders and Registrations bounded context now uses an event store to persist the state from the write side. This event store is implemented using Azure table storage to store the events. The application uses the Azure Service Bus to provide its messaging infrastructure.

While you are exploring and testing the solution, you can run it locally, either using the Azure compute emulator or by running the ASP.NET MVC web application directly and running a console application that hosts the handlers and domain objects. When you run the application locally, you can use a local SQL Server Express database instead of SQL Database, use a simple messaging infrastructure implemented in a SQL Server Express database, and a simple event store also implemented using a SQL Server Express database.

Note

The SQL-based implementations of the event store and the messaging infrastructure are only intended to help you run the application locally for exploration and testing. They are not intended to illustrate a production-ready approach.

For more information about the options for running the application, see Appendix 1, "Release Notes."

Conference Management bounded context

The Conference Management bounded context is a simple two-tier, create/read/update (CRUD)-style web application. It is implemented using ASP.NET MVC 4 and Entity Framework.

JJ591563.note(en-us,PandP.10).gifJana Says:
Jana
                The team implemented this bounded context after it implemented the public conference management website that uses ASP.NET MVC 3. In a later stage of the journey, as part of the V3 release, the conference management site will be upgraded to ASP.NET MVC 4.</td>

This bounded context must integrate with other bounded contexts that implement the CQRS pattern.

Patterns and concepts

This section describes some of the key areas of the application that the team visited during this stage of the journey and introduces some of the challenges met by the team when we addressed these areas.

Event sourcing

The team at Contoso originally implemented the Orders and Registrations bounded context without using event sourcing. However, during the implementation it became clear that using event sourcing would help to simplify this bounded context.

In Chapter 4, "Extending and Enhancing the Orders and Registrations Bounded Contexts," the team found that we needed to use events to push changes from the write side to the read side. On the read side, the OrderViewModelGenerator class subscribed to the events published by the Order aggregate, and used those events to update the views in the database that were queried by the read model.

This was already half way to an event-sourcing implementation, so it made sense to use a single persistence mechanism based on events for the whole bounded context.

The event sourcing infrastructure is reusable in other bounded contexts, and the implementation of the Orders and Registrations becomes simpler.

JJ591563.note(en-us,PandP.10).gifPoe Says:
Poe
                As a practical problem, the team had limited time before the V1 release to implement a production-quality event store. They created a simple, basic event store based on Azure tables as an interim solution. However, they will potentially face the problem in the future of migrating from one event store to another.</td>

Evolution is key here; for example, one could show how implementing event sourcing allows you to get rid of those tedious data migrations, and even allows you to build reports from the past.
—Tom Janssens - CQRS Advisors Mail List

The team implemented the basic event store using Azure table storage. If you are hosting your application in Azure, you could also consider using Azure blobs or SQL Database to store your events.

When choosing the underlying technology for your event store, you should ensure that your choice can deliver the level of availability, consistency, reliability, scale, and performance your application requires.

JJ591563.note(en-us,PandP.10).gifJana Says:
Jana
                One of the issues to consider when choosing between storage mechanisms in Azure is cost. If you use SQL Database you are billed based on the size of the database, if you use Azure table or blob storage you are billed based on the amount of storage you use and the number of storage transactions. You need to carefully evaluate the usage patterns on the different aggregates in your system to determine which storage mechanism is the most cost effective. It may turn out that different storage mechanisms make sense for different aggregate types. You may be able to introduce optimizations that lower your costs, for example by using caching to reduce the number of storage transactions.</td>

My rule of thumb is that if you're doing green-field development, you need very good arguments in order to choose a SQL Database. Azure Storage Services should be the default choice. However, if you already have an existing SQL Server database that you want to move to the cloud, it's a different case.
—Mark Seemann - CQRS Advisors Mail List

Identifying aggregates

In the Azure table storage-based implementation of the event store that the team created for the V1 release, we used the aggregate ID as the partition key. This makes it efficient to locate the partition that holds the events for any particular aggregate.

In some cases, the system must locate related aggregates. For example, an order aggregate may have a related registrations aggregate that holds details of the attendees assigned to specific seats. In this scenario, the team decided to reuse the same aggregate ID for the related pair of aggregates (the Order and Registration aggregates) in order to facilitate look-ups.

JJ591563.note(en-us,PandP.10).gifGary Says:
Gary
                You want to consider in this case whether you should have two aggregates. You could model the registrations as an entity inside the <strong>Order </strong>aggregate.</td>

A more common scenario is to have a one-to-many relationship between aggregates instead of a one-to-one. In this case, it is not possible to share aggregate IDs; instead, the aggregate on the "one side" can store a list of the IDs of the aggregates on the "many side," and each aggregate on the "many side" can store the ID of the aggregate on the "one side."

Sharing aggregate IDs is common when the aggregates exist in different bounded contexts. If you have aggregates in different bounded contexts that model different facets of the same real-world entity, it makes sense for them to share the same ID. This makes it easier to follow a real-world entity as different bounded contexts in your system process it.
—Greg Young - Conversation with the patterns & practices team

Task-based UI

The design of UIs has improved greatly over the last decade. Applications are easier to use, more intuitive, and simpler to navigate than they were before. Some examples of UI design guidelines that can help you create such modern, user-friendly apps are the Microsoft Inductive User Interface Guidelines and the Index of UX guidelines.

An important factor that affects the design and usability of the UI is how the UI communicates with the rest of the application. If the application is based on a CRUD-style architecture, this can leak through to the UI. If the developers focus on CRUD-style operations, this can result in a UI that looks like the one shown in the first screen design in Figure 2 (on the left).

Follow link to expand image

Figure 2

Example UIs for conference registration

On the first screen, the labels on the buttons reflect the underlying CRUD operations that the system will perform when the user clicks the Submit button, rather than displaying more user-focused action words. Unfortunately, the first screen also requires the user to apply some deductive knowledge about how the screen and the application function. For example, the function of the Add button is not immediately apparent.

A typical implementation behind the first screen will use a data transfer object (DTO) to exchange data between the back end and the UI. The UI will request data from the back end that will arrive encapsulated in a DTO, it will modify the data in the DTO, and then return the DTO to the back end. The back end will use the DTO to figure out what CRUD operations it must perform on the underlying data store.

The second screen is more explicit about what is happening in terms of the business process: the user is selecting quantities of seat types as a part of the conference registration task. Thinking about the UI in terms of the task that the user is performing makes it easier to relate the UI to the write model in your implementation of the CQRS pattern. The UI can send commands to the write side, and those commands are a part of the domain model on the write side. In a bounded context that implements the CQRS pattern, the UI typically queries the read side and receives a DTO, and sends commands to the write side.

Follow link to expand image

Figure 3

Task-based UI flow

Figure 3 shows a sequence of pages that enable the registrant to complete the "purchase seats at a conference" task. On the first page, the registrant selects the type and quantity of seats. On the second page, the registrant can review the seats she has reserved, enter her contact details, and complete the necessary payment information. The system then redirects the registrant to a payment provider, and if the payment completes successfully, the system displays the third page. The third page shows a summary of the order and provides a link to pages where the registrant can start additional tasks.

The sequence shown in Figure 3 is deliberately simplified in order to highlight the roles of the commands and queries in a task-based UI. For example, the real flow includes pages that the system will display based on the payment type selected by the registrant, and error pages that the system displays if the payment fails.

JJ591563.note(en-us,PandP.10).gifGary Says:
Gary
                You don't always need to use task-based UIs. In some scenarios, simple CRUD-style UIs work well. You must evaluate whether the benefits of task-based UIs outweigh the additional implementation effort required. Very often, the bounded contexts where you choose to implement the CQRS pattern are also the bounded contexts that benefit from task-based UIs because of the more complex business logic and more complex user interactions.</td>

I would like to state once and for all that CQRS does not require a task-based UI. We could apply CQRS to a CRUD based interface (though things like creating separated data models would be much harder).
There is, however, one thing that does really require a task based UI. That is domain-driven design.
—Greg Young, CQRS, Task Based UIs, Event Sourcing agh!.

For more information, see Chapter 4, "A CQRS and ES Deep Dive" in the Reference Guide.

CRUD

You should not use the CQRS pattern as part of your top-level architecture; you should implement the pattern only in those bounded contexts where it brings clear benefits. In the Contoso Conference Management System, the Conference Management bounded context is a relatively simple, stable, and low-volume part of the overall system. Therefore, the team decided that we would implement this bounded context using a traditional two-tier, CRUD-style architecture.

For a discussion about when CRUD-style architecture is, or is not, appropriate see the blog post, Why CRUD might be what they want, but may not be what they need.

Integration between bounded contexts

The Conference Management bounded context needs to integrate with the Orders and Registrations bounded context. For example, if the business customer changes the quota for a seat type in the Conference Management bounded context, this change must be propagated to the Orders and Registrations bounded context. Also, if a registrant adds a new attendee to a conference, the Business Customer must be able to view details of the attendee in the list in the conference management website.

Pushing changes from the Conference Management bounded context

The following conversation between several developers and the domain expert highlights some of the key issues that the team needed to address in planning how to implement this integration.
Developer 1: I want to talk about how we should implement two pieces of the integration story associated with our CRUD-style, Conference Management bounded context. First of all, when a business customer creates a new conference or defines new seat types for an existing conference in this bounded context, other bounded contexts such as the Orders and Registrations bounded context will need to know about the change. Secondly, when a business customer changes the quota for a seat type, other bounded contexts will need to know about this change as well.
Developer 2: So in both cases you are pushing changes from the Conference Management bounded context. It's one way.
Developer 1: Correct.
Developer 2: What are the significant differences between the scenarios you outlined?
Developer 1: In the first scenario, these changes are relatively infrequent and typically happen when the business customer creates the conference. Also, these are append-only changes. We don't allow a business customer to delete a conference or a seat type after the conference has been published for the first time. In the second scenario, the changes might be more frequent and a business customer might increase or decrease a seat quota.
Developer 2: What implementation approaches are you considering for these integration scenarios?
Developer 1: Because we have a two-tier CRUD-style bounded context, for the first scenario I was planning to expose the conference and seat-type information directly from the database as a simple read-only service. For the second scenario, I was planning to publish events whenever the business customer updates the seat quotas.
Developer 2: Why use two different approaches here? It would be simpler to use a single approach. Using events is more flexible in the long run. If additional bounded contexts need this information, they can easily subscribe to the event. Using events provides for less coupling between the bounded contexts.
Developer 1: I can see that it would be easier to adapt to changing requirements in the future if we used events. For example, if a new bounded context required information about who changed the quota, we could add this information to the event. For existing bounded contexts, we could add an adapter that converted the new event format to the old.
Developer 2: You implied that the events that notify subscribers of quota changes would send the change that was made to the quota. For example, let's say the business customer increased a seat quota by 50. What happens if a subscriber wasn't there at the beginning and therefore doesn't receive the full history of updates?
Developer 1: We may have to include some synchronization mechanism that uses snapshots of the current state. However, in this case the event could simply report the new value of the quota. If necessary, the event could report both the delta and the absolute value of the seat quota.
Developer 2: How are you going to ensure consistency? You need to guarantee that your bounded context persists its data to storage and publishes the events on a message queue.
Developer 1: We can wrap the database write and add-to-queue operations in a transaction.
Developer 2: There are two reasons that's going to be problematic later when the size of the network increases, response times get longer, and the probability of failure increases. First, our infrastructure uses the Azure Service Bus for messages. You can't use a single transaction to combine the sending of a message on the Service Bus and a write to a database. Second, we're trying to avoid two-phase commits because they always cause problems in the long run.
Domain Expert: We have a similar scenario with another bounded context that we'll be looking at later. In this case, we can't make any changes to the bounded context; we no longer have an up-to-date copy of the source code.
Developer 1: What can we do to avoid using a two-phase commit? And what can we do if we don't have access to the source code and thus can't make any changes?
Developer 2: In both cases, we use the same technique to solve the problem. Instead of publishing the events from within the application code, we can use another process that monitors the database and sends the events when it detects a change in the database. This solution may introduce a small amount of latency, but it does avoid the need for a two-phase commit and you can implement it without making any changes to the application code.

Another issue concerns when and where to persist integration events. In the example discussed above, the Conference Management bounded context publishes the events and the Orders and Registrations bounded context handles them and uses them to populate its read model. If a failure occurs that causes the system to lose the read-model data, then without saving the events there is no way to recreate that read-model data.

Whether you need to persist these integration events will depend on the specific requirements and implementation of your application. For example:

  • The write side may handle the integration instead of the read side, as in the current example. The events will then result in changes on the write side that are persisted as other events.
  • Integration events may represent transient data that does not need to be persisted.
  • Integration events from a CRUD-style bounded context may contain state data so that only the last event is needed. For example if the event from the Conference Management bounded context includes the current seat quota, you may not be interested in previous values.

Another approach to consider is to use an event store that many bounded contexts share. In this way, the originating bounded context (for example the CRUD-style Conference Management bounded context) could be responsible for persisting the integration events.
—Greg Young - Conversation with the patterns & practices team.

Some comments on Azure Service Bus

The previous discussion suggested a way to avoid using a distributed two-phase commit in the Conference Management bounded context. However, there are alternative approaches.

Although the Azure Service Bus does not support distributed transactions that combine an operation on the bus with an operation on a database, you can use the RequiresDuplicateDetection property when you send messages, and the PeekLock mode when you receive messages to create the desired level of robustness without using a distributed transaction.

As an alternative, you can use a distributed transaction to update the database and send a message using a local Microsoft message queuing (MSMQ) queue. You can then use a bridge to connect the MSMQ queue to a Azure Service Bus queue.

For an example of implementing a bridge from MSMQ to Azure Service Bus, see the sample in the Microsoft Azure AppFabric SDK.

For more information about the Azure Service Bus, see Chapter 7, "Technologies Used in the Reference Implementation" in the Reference Guide.

Pushing changes to the Conference Management bounded context

Pushing information about completed orders and registrations from the Orders and Registrations bounded context to the Conference Management bounded context raised a different set of issues.

The Orders and Registrations bounded context typically raises many of the following events during the creation of an order: OrderPlaced, OrderRegistrantAssigned, OrderTotalsCalculated, OrderPaymentConfirmed, SeatAssignmentsCreated, SeatAssignmentUpdated, SeatAssigned, and SeatUnassigned. The bounded context uses these events to communicate between aggregates and for event sourcing.

For the Conference Management bounded context to capture the information it requires to display details about registrations and attendees, it must handle all of these events. It can use the information that these events contain to create a denormalized SQL table of the data, which the business customer can then view in the UI.

The issue with this approach is that the Conference Management bounded context needs to understand a complex set of events from another bounded context. It is a brittle solution because a change in the Orders and Registrations bounded context may break this feature in the Conference Management bounded context.

Contoso plans to keep this solution for the V1 release of the system, but will evaluate alternatives during the next stage of the journey. These alternative approaches will include:

  • Modifying the Orders and Registrations bounded context to generate more useful events designed explicitly for integration.
  • Generating the denormalized data in the Orders and Registrations bounded context and notifying the Conference Management bounded context when the data is ready. The Conference Management bounded context can then request the information through a service call.

Note

To see how the current approach works, look at the OrderEventHandler class in the Conference project.

Choosing when to update the read-side data

In the Conference Management bounded context, the business customer can change the description of a seat type. This results in a SeatUpdated event that the ConferenceViewModelGenerator class in the Orders and Registrations bounded context handles; this class updates the read-model data to reflect the new information about the seat type. The UI displays the new seat description when a registrant is making an order.

However, if a registrant views a previously created order (for example to assign attendees to seats), the registrant sees the original seat description.

JJ591563.note(en-us,PandP.10).gifCarlos Says:
Carlos
                This is a deliberate business decision; we don't want to confuse registrants by changing the seat description after they create an order.</td>
JJ591563.note(en-us,PandP.10).gifGary Says:
Gary
                If we did want to update the seat description on existing orders, we would need to modify the <strong>PricedOrderViewModelGenerator</strong> class to handle the <strong>SeatUpdated</strong> event and adjust its view model.</td>

Distributed transactions and event sourcing

The previous section that discussed the integration options for the Conference Management bounded context raised the issue of using a distributed, two-phase commit transaction to ensure consistency between the database that stores the conference management data and the messaging infrastructure that publishes changes to other bounded contexts.

The same problem arises when you implement event sourcing: you must ensure consistency between the event store in the bounded context that stores all the events and the messaging infrastructure that publishes those events to other bounded contexts.

A key feature of an event store implementation should be that it offers a way to guarantee consistency between the events that it stores and the events that the bounded context publishes to other bounded contexts.

JJ591563.note(en-us,PandP.10).gifCarlos Says:
Carlos
                This is a key challenge you should address if you decide to implement an event store yourself. If you are designing a scalable event store that you plan to deploy in a distributed environment such as Azure, you must be very careful to ensure that you meet this requirement.</td>

Autonomy versus authority

The Orders and Registrations bounded context is responsible for creating and managing orders on behalf of registrants. The Payments bounded context is responsible for managing the interaction with an external payments system so that registrants can pay for the seats that they have ordered.

When the team was examining the domain models for these two bounded contexts, it discovered that neither context knew anything about pricing. The Orders and Registrations bounded context created an order that listed the quantities of the different seat types that the registrant requested. The Payments bounded context simply passed a total to the external payments system. At some point, the system needed to calculate the total from the order before invoking the payment process.

The team considered two different approaches to solve this problem: favoring autonomy and favoring authority.

Favoring autonomy

The autonomous approach assigns the responsibility for calculating the order total to the Orders and Registrations bounded context. The Orders and Registrations bounded context is not dependent on another bounded context when it needs to perform the calculation because it already has the necessary data. At some point in the past, it will have collected the pricing information it needs from other bounded contexts (such as the Conference Management bounded context) and cached it.

The advantage of this approach is that the Orders and Registrations bounded context is autonomous. It doesn't rely on the availability of another bounded context or service.

The disadvantage is that the pricing information could be out of date. The business customer might have changed the pricing information in the Conference Management bounded context, but that change might not yet have reached the Orders and Registrations bounded context.

Favoring authority

In this approach, the part of the system that calculates the order total obtains the pricing information from the bounded contexts (such as the Conference Management bounded context) at the point in time that it performs the calculation. The Orders and Registrations bounded context could still perform the calculation, or it could delegate the calculation to another bounded context or service within the system.

The advantage of this approach is that the system always uses the latest pricing information whenever it is calculating an order total.

The disadvantage is that the Orders and Registrations bounded context is dependent on another bounded context when it needs to determine the total for the order. It either needs to query the Conference Management bounded context for the up-to-date pricing information, or call another service that performs the calculation.

Choosing between autonomy and authority

The choice between the two alternatives is a business decision. The specific business requirements of your scenario should determine which approach to take. Autonomy is often the preference for large, online systems.

JJ591563.note(en-us,PandP.10).gifJana Says:
Jana
                This choice may change depending on the state of your system. Consider an overbooking scenario. The autonomy strategy may optimize for the normal case when lots of conference seats are still available, but as a particular conference fills up, the system may need to become more conservative and favor authority, using the latest information on seat availability.</td>

The way that the conference management system calculates the total for an order represents an example of choosing autonomy over authority.

JJ591563.note(en-us,PandP.10).gifCarlos Says:
Carlos
                For Contoso, the clear choice is autonomy. It's a serious problem if registrants can't purchase seats because some other bounded context is down. However, we don't really care if there's a short lag between the business customer modifying the pricing information, and that new pricing information being used to calculate order totals.</td>

The section Calculating totals below describes how the system performs this calculation.

Approaches to implementing the read side

In the discussions of the read side in the previous chapters, you saw how the team used a SQL-based store for the denormalized projections of the data from the write side.

You can use other storage mechanisms for the read-model data; for example, you can use the file system or Azure table or blob storage. In the Orders and Registrations bounded context, the system uses Azure blobs to store information about the seat assignments.

JJ591563.note(en-us,PandP.10).gifGary Says:
Gary
                When you are choosing the underlying storage mechanism for the read side, you should consider the costs associated with the storage (especially in the cloud) in addition to the requirement that the read-side data should be easy and efficient to access using the queries on the read side.</td>

Note

See the SeatAssignmentsViewModelGenerator class to understand how the data is persisted to blob storage and the SeatAssignmentsDao class to understand how the UI retrieves the data for display.

Eventual consistency

During testing, the team discovered a scenario in which the registrant might see evidence of eventual consistency in action. If the registrant assigns attendees to seats on an order and then quickly navigates to view the assignments, then sometimes this view shows only some of the updates. However, refreshing the page displays the correct information. This happens because it takes time for the events that record the seat assignments to propagate to the read model, and sometimes the tester viewed the information queried from the read model too soon.

The team decided to add a note to the view page warning users about this possibility, although a production system is likely to update the read model faster than a debug version of the application running locally.

JJ591563.note(en-us,PandP.10).gifCarlos Says:
Carlos
                So long as the registrant knows that the changes have been persisted, and that what the UI displays could be a few seconds out of date, they are not going to be concerned.</td>

Implementation details

This section describes some of the significant features of the implementation of the Orders and Registrations bounded context. You may find it useful to have a copy of the code so you can follow along. You can download a copy from the Download center, or check the evolution of the code in the repository on GitHub: https://github.com/mspnp/cqrs-journey-code. You can download the code from the V1 release from the Tags page on GitHub.

Note

Do not expect the code samples to match exactly the code in the reference implementation. This chapter describes a step in the CQRS journey, the implementation may well change as we learn more and refactor the code.

The Conference Management bounded context

The Conference Management bounded context that enables a business customer to define and manage conferences is a simple two-tier, CRUD-style application that uses ASP.NET MVC 4.

In the Visual Studio solution, the Conference project contains the model code, and the Conference.Web project contains the MVC views and controllers.

Integration with the Orders and Registration bounded context

The Conference Management bounded context pushes notifications of changes to conferences by publishing the following events.

  • ConferenceCreated. Published whenever a business customer creates a new conference.
  • ConferenceUpdated. Published whenever a business customer updates an existing conference.
  • ConferencePublished. Published whenever a business customer publishes a conference.
  • ConferenceUnpublished. Published whenever a business customer unpublishes a new conference.
  • SeatCreated. Published whenever a business customer defines a new seat type.
  • SeatsAdded. Published whenever a business customer increases the quota of a seat type.

The ConferenceService class in the Conference project publishes these events to the event bus.

JJ591563.note(en-us,PandP.10).gifMarkus Says:
Markus
                At the moment, there is no distributed transaction to wrap the database update and the message publishing.</td>

The Payments bounded context

The Payments bounded context is responsible for handling the interaction with the external systems that validate and process payments. In the V1 release, payments can be processed either by a fake, external, third-party payment processor (that mimics the behavior of systems such as PayPal) or by an invoicing system. The external systems can report either that a payment was successful or that it failed.

The sequence diagram in Figure 4 illustrates how the key elements that are involved in the payment process interact with each other. The diagram is shows a simplified view, ignoring the handler classes to better describe the process.

Follow link to expand image

Figure 4

Overview of the payment process

Figure 4 shows how the Orders and Registrations bounded context, the Payments bounded context, and the external payments service all interact with each other. In the future, registrants will also be able to pay by invoice instead of using a third-party payment processing service.

The registrant makes a payment as a part of the overall flow in the UI, as shown in Figure 3. The PaymentController controller class does not display a view unless it has to wait for the system to create the ThirdPartyProcessorPayment aggregate instance. Its role is to forward payment information collected from the registrant to the third-party payment processor.

Typically, when you implement the CQRS pattern, you use events as the mechanism for communicating between bounded contexts. However, in this case, the RegistrationController and PaymentController controller classes send commands to the Payments bounded context. The Payments bounded context does use events to communicate with the RegistrationProcessManager instance in the Orders and Registrations bounded context.

The implementation of the Payments bounded context implements the CQRS pattern without event sourcing.

The write-side model contains an aggregate called ThirdPartyProcessorPayment that consists of two classes: ThirdPartyProcessorPayment and ThirdPartyProcessorPaymentItem. Instances of these classes are persisted to a SQL Database instance by using Entity Framework. The PaymentsDbContext class implements an Entity Framework context.

The ThirdPartyProcessorPaymentCommandHandler implements a command handler for the write side.

The read-side model is also implemented using Entity Framework. The PaymentDao class exposes the payment data on the read side. For an example, see the GetThirdPartyProcessorPaymentDetails method.

Figure 5 illustrates the different parts that make up the read side and the write side of the Payments bounded context.

Follow link to expand image

Figure 5

The read side and the write side in the Payments bounded context

Integration with online payment services, eventual consistency, and command validation

Typically, online payment services offer two levels of integration with your site:

  • The simple approach, for which you don't need a merchant account with the payments provider, works through a simple redirect mechanism. You redirect your customer to the payment service. The payment service takes the payment, and then redirects the customer back to a page on your site along with an acknowledgement code.
  • The more sophisticated approach, for which you do need a merchant account, is based on an API. It typically executes in two steps. First, the payment service verifies that your customer can pay the required amount, and sends you a token. Second, you can use the token within a fixed time to complete the payment by sending the token back to the payment service.

Contoso assumes that its business customers do not have a merchant account and must use the simple approach. One consequence of this is that a seat reservation could expire while the customer is completing the payment. If this happens, the system tries to re-acquire the seats after the customer makes the payment. In the event that the seats cannot be re-acquired, the system notifies the business customer of the problem and the business customer must resolve the situation manually.

Note

The system allows a little extra time over and above the time shown in the countdown clock to allow payment processing to complete.

This specific scenario, in which the system cannot make itself fully consistent without a manual intervention by a user (in this case the business owner, who must initiate a refund or override the seat quota) illustrates the following more general point in relation to eventual consistency and command validation.

A key benefit of embracing eventual consistency is to remove the requirement for using distributed transactions, which have a significant, negative impact on the scalability and performance of large systems because of the number and duration of locks they must hold in the system. In this specific scenario, you could take steps to avoid the potential problem of accepting payment without seats being available in two ways:

  • Change the system to re-check the seat availability just before completing the payment. This is not possible because of the way that the integration with the payments system works without a merchant account.
  • Keep the seats reserved (locked) until the payment is complete. This is difficult because you do not know how long the payment process will take; you must reserve (lock) the seats for an indeterminate period while you wait for the registrant to complete the payment.

The team chose to allow for the possibility that a registrant could pay for seats only to find that they are no longer available; in addition to being very unlikely in practice because a timeout would have to occur while a registrant is paying for the very last seats, this approach has the smallest impact on the system because it doesn't require a long-term reservation (lock) on any seats.

JJ591563.note(en-us,PandP.10).gifMarkus Says:
Markus
                To minimize further the chance of this scenario occurring, the team decided to increase the buffer time for releasing reserved seats from five minutes to fourteen minutes. The original value of five minutes was chosen to account for any possible clock skew between the servers so that reservations were not released before the fifteen-minute countdown timer in the UI expired.</td>

In more general terms, you could restate the two options above as:

  • Validate commands just before they execute to try to ensure that the command will succeed.
  • Lock all the resources until the command completes.

If the command only affects a single aggregate and does not need to reference anything outside of the consistency boundary defined by the aggregate, then there is no problem because all of the information required to validate the command is within the aggregate. This is not the case in the current scenario; if you could validate whether the seats were still available just before you made the payment, this check would involve checking information from outside the current aggregate.

If, in order to validate the command you need to look at data outside of the aggregate, for example, by querying a read model or by looking in a cache, the scalability of the system is going to be negatively impacted. Also, if you are querying a read model, remember that read models are eventually consistent. In the current scenario, you would need to query an eventually consistent read model to check on the seats availability.

If you decide to lock all of the relevant resources until the command completes, be aware of the impact this will have on the scalability of your system.

It is far better to handle such a problem from a business perspective than to make large architectural constraints upon our system.
—Greg Young.
For a detailed discussion of this issue, see Q/A Greg Young's Blog.

Event sourcing

The initial implementation of the event sourcing infrastructure is extremely basic: the team intends to replace it with a production-quality event store in the near future. This section describes the initial, basic implementation and lists the various ways to improve it.

The core elements of this basic event sourcing solution are that:

  • Whenever the state of an aggregate instance changes, the instance raises an event that fully describes the state change.
  • The system persists these events in an event store.
  • An aggregate can rebuild its state by replaying its past stream of events.
  • Other aggregates and process managers (possibly in different bounded contexts) can subscribe to these events.

Raising events when the state of an aggregate changes

The following two methods from the Order aggregate are examples of methods that the OrderCommandHandler class invokes when it receives a command for the order. Neither of these methods updates the state of the Order aggregate; instead, they raise an event that will be handled by the Order aggregate. In the MarkAsReserved method, there is some minimal logic to determine which of two events to raise.

public void MarkAsReserved(DateTime expirationDate, IEnumerable<SeatQuantity> reservedSeats)
{
    if (this.isConfirmed)
        throw new InvalidOperationException("Cannot modify a confirmed order.");

    var reserved = reservedSeats.ToList();

    // Is there an order item which didn't get an exact reservation?
    if (this.seats.Any(item => !reserved.Any(seat => seat.SeatType == item.SeatType && seat.Quantity == item.Quantity)))
    {
        this.Update(new OrderPartiallyReserved { ReservationExpiration = expirationDate, Seats = reserved.ToArray() });
    }
    else
    {
        this.Update(new OrderReservationCompleted { ReservationExpiration = expirationDate, Seats = reserved.ToArray() });
    }
}

public void ConfirmPayment()
{
    this.Update(new OrderPaymentConfirmed());
}

The abstract base class of the Order class defines the Update method. The following code sample shows this method and the Id and Version properties in the EventSourced class.

private readonly Guid id;
private int version = -1;

protected EventSourced(Guid id)
{
    this.id = id;
}

public int Version { get { return this.version; } }

protected void Update(VersionedEvent e)
{
    e.SourceId = this.Id;
    e.Version = this.version + 1;
    this.handlers[e.GetType()].Invoke(e);
    this.version = e.Version;
    this.pendingEvents.Add(e);
}

The Update method sets the Id and increments the version of the aggregate. It also determines which of the event handlers in the aggregate it should invoke to handle the event type.

JJ591563.note(en-us,PandP.10).gifMarkus Says:
Markus Every time the system updates the state of an aggregate, it increments the version number of the aggregate.

The following code sample shows the event handler methods in the Order class that are invoked when the command methods shown above are called.

private void OnOrderPartiallyReserved(OrderPartiallyReserved e)
{
    this.seats = e.Seats.ToList();
}

private void OnOrderReservationCompleted(OrderReservationCompleted e)
{
    this.seats = e.Seats.ToList();
}

private void OnOrderExpired(OrderExpired e)
{
}

private void OnOrderPaymentConfirmed(OrderPaymentConfirmed e)
{
    this.isConfirmed = true;
}

These methods update the state of the aggregate.

An aggregate must be able to handle both events from other aggregates and events that it raises itself. The protected constructor in the Order class lists all the events that the Order aggregate can handle.

protected Order()
{
    base.Handles<OrderPlaced>(this.OnOrderPlaced);
    base.Handles<OrderUpdated>(this.OnOrderUpdated);
    base.Handles<OrderPartiallyReserved>(this.OnOrderPartiallyReserved);
    base.Handles<OrderReservationCompleted>(this.OnOrderReservationCompleted);
    base.Handles<OrderExpired>(this.OnOrderExpired);
    base.Handles<OrderPaymentConfirmed>(this.OnOrderPaymentConfirmed);
    base.Handles<OrderRegistrantAssigned>(this.OnOrderRegistrantAssigned);
}

Persisting events to the event store

When the aggregate processes an event in the Update method in the EventSourcedAggregateRoot class, it adds the event to a private list of pending events. This list is exposed as a public, IEnumerable property of the abstract EventSourced class called Events.

The following code sample from the OrderCommandHandler class shows how the handler invokes a method in the Order class to handle a command, and then uses a repository to persist the current state of the Order aggregate by appending all pending events to the store.

public void Handle(MarkSeatsAsReserved command)
{
    var order = repository.Find(command.OrderId);

    if (order != null)
    {
        order.MarkAsReserved(command.Expiration, command.Seats);
        repository.Save(order);
    }
}

The following code sample shows the initial simple implementation of the Save method in the SqlEventSourcedRepository class.

Note

These examples refer to a SQL Server-based event store. This was the initial approach that was later replaced with an implementation based on Azure table storage. The SQL Server-based event store remains in the solution as a convenience; you can run the application locally and use this implementation to avoid any dependencies on Azure.

public void Save(T eventSourced)
{
    // TODO: guarantee that only incremental versions of the event are stored
    var events = eventSourced.Events.ToArray();
    using (var context = this.contextFactory.Invoke())
    {
        foreach (var e in events)
        {
            using (var stream = new MemoryStream())
            {
                this.serializer.Serialize(stream, e);
                var serialized = new Event { AggregateId = e.SourceId, Version = e.Version, Payload = stream.ToArray() };
                context.Set<Event>().Add(serialized);
            }
        }

        context.SaveChanges();
    }

    // TODO: guarantee delivery or roll back, or have a way to resume after a system crash
    this.eventBus.Publish(events);
}

Replaying events to rebuild state

When a handler class loads an aggregate instance from storage, it loads the state of the instance by replaying the stored event stream.

JJ591563.note(en-us,PandP.10).gifPoe Says:
Poe We later found that using event sourcing and being able to replay events was invaluable as a technique for analyzing bugs in the production system running in the cloud. We could make a local copy of the event store, then replay the event stream locally and debug the application in Visual Studio to understand exactly what happened in the production system.

The following code sample from the OrderCommandHandler class shows how calling the Find method in the repository initiates this process.

public void Handle(MarkSeatsAsReserved command)
{
    var order = repository.Find(command.OrderId);

    ...
}

The following code sample shows how the SqlEventSourcedRepository class loads the event stream associated with the aggregate.

JJ591563.note(en-us,PandP.10).gifJana Says:
Jana The team later developed a simple event store using Azure tables instead of the SqlEventSourcedRepository. The next section describes this Azure table storage-based implementation.
public T Find(Guid id)
{
    using (var context = this.contextFactory.Invoke())
    {
        var deserialized = context.Set<Event>()
            .Where(x => x.AggregateId == id)
            .OrderBy(x => x.Version)
            .AsEnumerable()
            .Select(x => this.serializer.Deserialize(new MemoryStream(x.Payload)))
            .Cast<IVersionedEvent>()
            .AsCachedAnyEnumerable();

        if (deserialized.Any())
        {
            return entityFactory.Invoke(id, deserialized);
        }

        return null;
    }
}

The following code sample shows the constructor in the Order class that rebuilds the state of the order from its event stream when it is invoked by the Invoke method in the previous code sample.

public Order(Guid id, IEnumerable<IVersionedEvent> history) : this(id)
{
    this.LoadFrom(history);
}

The LoadFrom method is defined in the EventSourced class, as shown in the following code sample. For each stored event in the history, it determines the appropriate handler method to invoke in the Order class and updates the version number of the aggregate instance.

protected void LoadFrom(IEnumerable<IVersionedEvent> pastEvents)
{
    foreach (var e in pastEvents)
    {
        this.handlers[e.GetType()].Invoke(e);
        this.version = e.Version;
    }
}

Issues with the simple event store implementation

The simple implementation of event sourcing and an event store outlined in the previous sections has a number of shortcomings. The following list identifies some of these shortcomings that should be overcome in a production-quality implementation.

  • There is no guarantee in the Save method in the SqlEventRepository class that the event is persisted to storage and published to the messaging infrastructure. A failure could result in an event being saved to storage but not being published.
  • There is no check that when the system persists an event, that it is a later event than the previous one. Potentially, events could be stored out of sequence.
  • There are no optimizations in place for aggregate instances that have a large number of events in their event stream. This could result in performance problems when replaying events.

Azure table storage-based event store

The Azure table storage-based event store addresses some of the shortcomings of the simple SQL Server-based event store. However, at this point in time, it is still not a production-quality implementation.

The team designed this implementation to guarantee that events are both persisted to storage and published on the message bus. To achieve this, it uses the transactional capabilities of Azure tables.

JJ591563.note(en-us,PandP.10).gifMarkus Says:
Markus Azure table storage supports transactions across records that share the same partition key.

The EventStore class initially saves two copies of every event to be persisted. One copy is the permanent record of that event, and the other copy becomes part of a virtual queue of events that must be published on the Azure Service Bus. The following code sample shows the Save method in the EventStore class. The prefix "Unpublished" identifies the copy of the event that is part of the virtual queue of unpublished events.

public void Save(string partitionKey, IEnumerable<EventData> events)
{
    var context = this.tableClient.GetDataServiceContext();
    foreach (var eventData in events)
    {
        var formattedVersion = eventData.Version.ToString("D10");
        context.AddObject(
            this.tableName,
            new EventTableServiceEntity
                {
                    PartitionKey = partitionKey,
                    RowKey = formattedVersion,
                    SourceId = eventData.SourceId,
                    SourceType = eventData.SourceType,
                    EventType = eventData.EventType,
                    Payload = eventData.Payload
                });

        // Add a duplicate of this event to the Unpublished "queue"
        context.AddObject(
            this.tableName,
            new EventTableServiceEntity
            {
                PartitionKey = partitionKey,
                RowKey = UnpublishedRowKeyPrefix + formattedVersion,
                SourceId = eventData.SourceId,
                SourceType = eventData.SourceType,
                EventType = eventData.EventType,
                Payload = eventData.Payload
            });

    }

    try
    {
        this.eventStoreRetryPolicy.ExecuteAction(() => context.SaveChanges(SaveChangesOptions.Batch));
    }
    catch (DataServiceRequestException ex)
    {
        var inner = ex.InnerException as DataServiceClientException;
        if (inner != null && inner.StatusCode == (int)HttpStatusCode.Conflict)
        {
            throw new ConcurrencyException();
        }

        throw;
    }
}

Note

This code sample also illustrates how a duplicate key error is used to identify a concurrency error.

The Save method in the repository class is shown below. This method is invoked by the event handler classes, invokes the Save method shown in the previous code sample, and invokes the SendAsync method of the EventStoreBusPublisher class.

public void Save(T eventSourced)
{
    var events = eventSourced.Events.ToArray();
    var serialized = events.Select(this.Serialize);

    var partitionKey = this.GetPartitionKey(eventSourced.Id);
    this.eventStore.Save(partitionKey, serialized);

    this.publisher.SendAsync(partitionKey);
}

The EventStoreBusPublisher class is responsible for reading the unpublished events for the aggregate from the virtual queue in the Azure table store, publishing the event on the Azure Service Bus, and then deleting the unpublished event from the virtual queue.

If the system fails between publishing the event on the Azure Service Bus and deleting the event from the virtual queue then, when the application restarts, the event is published a second time. To avoid problems caused by duplicate events, the Azure Service Bus is configured to detect duplicate messages and ignore them.

JJ591563.note(en-us,PandP.10).gifMarkus Says:
Markus In the case of a failure, the system must include a mechanism for scanning all of the partitions in table storage for aggregates with unpublished events and then publishing those events. This process will take some time to run, but will only need to run when the application restarts.

Calculating totals

To ensure its autonomy, the Orders and Registrations bounded context calculates order totals without accessing the Conference Management bounded context. The Conference Management bounded context is responsible for maintaining the prices of seats for conferences.

Whenever a business customer adds a new seat type or changes the price of a seat, the Conference Management bounded context raises an event. The Orders and Registrations bounded context handles these events and persists the information as part of its read model (see the ConferenceViewModelGenerator class in the reference implementation solution for details).

When the Order aggregate calculates the order total, it uses the data provided by the read model. See the MarkAsReserved method in the Order aggregate and the PricingService class for details.

JJ591563.note(en-us,PandP.10).gifJana Says:
Jana The UI also displays a dynamically calculated total as the registrant adds seats to an order. The application calculates this value using JavaScript. When the registrant makes a payment, the system uses the total that the Order aggregate calculates.

Impact on testing

JJ591563.note(en-us,PandP.10).gifMarkus Says:
Markus Don't let your passing unit tests lull you into a false sense of security. There are lots of moving parts when you implement the CQRS pattern. You need to test that they all work correctly together.
JJ591563.note(en-us,PandP.10).gifMarkus Says:
Markus Don't forget to create unit tests for your read models. A unit test on the read-model generator uncovered a bug just prior to the V1 release whereby the system removed order items when it updated an order.

Timing issues

One of the acceptance tests verifies the behavior of the system when a business customer creates new seat types. The key steps in the test create a conference, create a new seat type for the conference, and then publish the conference. This raises the corresponding sequence of events: ConferenceCreated, SeatCreated, and ConferencePublished.

The Orders and Registrations bounded context handles these integration events. The test determined that the Orders and Registrations bounded context received these events in a different order from the order that the Conference Management bounded context sent them.

The Azure Service Bus only offers best-effort first in first out (FIFO), therefore, it may not deliver events in the order in which they were sent. It is also possible in this scenario that the issue occurs because of the different times it takes for the steps in the test to create the messages and deliver them to the Azure Service Bus. The introduction of an artificial delay between the steps in the test provided a temporary solution to this problem.

In the V2 release, the team plans to address the general issue of message ordering and either modify the infrastructure to guarantee proper ordering or make the system more robust if messages do arrive out of order.

Involving the domain expert

In Chapter 4, "Extending and Enhancing the Orders and Registrations Bounded Contexts," you saw how the domain expert was involved with designing the acceptance tests and how his involvement helped clarify domain knowledge.

You should also ensure that the domain expert attends bug triage meetings. He or she can help clarify the expected behavior of the system, and during the discussion may uncover new user stories. For example, during the triage of a bug related to unpublishing a conference in the Conference Management bounded context, the domain expert identified a requirement to allow the business customer to add a redirect link for the unpublished conference to a new conference or alternate page.

Summary

During this stage of our journey, we completed our first pseudo-production release of the Contoso Conference Management System. It now comprises several integrated bounded contexts, a more polished UI, and uses event sourcing in the Orders and Registrations bounded context.

There is still more work for us to do, and the next chapter will describe the next stage in our CQRS journey as we head towards the V2 release and address the issues associated with versioning our system.

Next Topic | Previous Topic | Home | Community