1. CQRS / ES
by Vic Metcalfe / @v_metcalfe
Command / Query
Responsibility
Segregation, Event
Sourcing and a little
Domain Driven Design
2. Welcome
• Not an expert
• Over 60 slides, many containing code
• Ask questions at any time, expect short
answers / discussion
• Q & A after 30 minutes, or talk to me after
3. CRUD
CREATE / REPORT /
UPDATE / DELETE
UPDATE AND
DELETE DESTROY
DATA
HOW MUCH WAS
THAT DATA WORTH?
4. Treat the storage of
data as a separate
problem from the
retrieval of data
8. Domain Driven
Design
• Helps business stakeholders think about their
requirements
• Helps developers adapt their software models
to the problem domain
25. Discussion
Points
• Where should we place projectors?
• Should projections live in the same database
as our events?
• Batch and asynchronous projections
28. Discussion
Points
• Where should we place process managers?
• They look like projectors – why do we need
both?
• Why not just have process managers create
events directly?
32. Ubiquitous
Language
• Definitions that have specialized meaning
within our problem domain
• Updated as needed when terminology evolves
in a project team
33. Bounded
Context
• A specific problem domain
• A project may contain just one or many
• An aggregate is defined within a single
bounded context
40. Reliability
• The impact of bugs is reduced by the
immutability of event streams
• Lower complexity* makes bugs less likely and
code easier to test
* Assuming our application is non-trivial and that CQRS/ES results in
lower complexity!
41. Flexibility
• Separation of concerns
• Tends to be implemented in short simple
functions, or small simple classes
• “If we’d recorded that in the database it
wouldn’t be a problem”
• “That report will be slow because of too many
joins”
42. Scaling
• Different aggregate root types can use
different event stores / databases
• Projections can live on different databases
• Projections can use the most appropriate
database technology (relational, no-SQL,
graph, etc.…)
43. Security
• Event store can be read and append only
• Projections can be audited
• Web application can have read-only rights to
projections
45. Feature: Register a new client
So that I can borrow books, as a guest I need to create an account on the system.
Background:
Given that I am a guest on the registration page
And I have filled in "pat@example.com" for my email address
And I have filled in "letmein" for my password
Scenario: Register a new Client
When I click the register button
Then I receive an email to "pat@example.com" asking me to confirm my email address
And my password is "letmein"
Scenario: Confirm Email Address
Given I click the register button
And I receive a confirmation email
When I follow the link in the confirmation email
Then my account is confirmed
48. public function register($user, $id, $email, $hashedPassword, $token)
{
if (!is_null($user)) {
throw new InvariantException("Only new users can register. " .
"User $id has already been registered.");
}
$stmt = $this->db->prepare("select id from user where confirmed_email=:email");
$stmt->execute(['email'=>$email]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row !== false) {
throw new EmailAlreadyRegisteredException();
}
Projection
src/Model/User/UserCommands.php
49. $stmt = $this->db->prepare("select id from user where requested_email=:email");
$stmt->execute(['email'=>$email]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row !== false) {
throw new EmailAlreadyPendingException();
}
return Event::NamedPayloadPairsToArray(
User::REGISTERED, [],
User::PROVIDED_EMAIL_ADDRESS, ['email' => $email, 'token'=>$token],
User::CHANGED_PASSWORD, ['password'=>$hashedPassword]
);
Projection
src/Model/User/UserCommands.php
There’s a much better way to enforce unique emails by using a
separate aggregate root with email keys and Associate /
Disassociate commands, but that wouldn’t fit into a ½ hour talk!
51. /** @Projector(User::REGISTERED) */
public function OnRegistered($id)
{
$stmt = $this->connection->prepare("insert into user (id) values (:id)");
$stmt->execute(['id'=>$id]);
}
src/Model/User/Projector/UserListProjector.php
52. /** @Projector(User::PROVIDED_EMAIL_ADDRESS) */
public function OnProvidedEmailAddress($id, Event $event)
{
$payload = $event->getPayload();
$stmt = $this->connection->prepare(
"update user set requested_email=:email where id=:id");
$stmt->execute(['id'=>$id, 'email'=>$payload['email']]);
}
src/Model/User/Projector/UserListProjector.php
53. /** @Projector(User::CHANGED_PASSWORD) */
public function OnChangedPassword($id, Event $event)
{
$payload = $event->getPayload();
$stmt = $this->connection->prepare(
"update user set password=:password where id=:id");
$stmt->execute(['id'=>$id, 'password'=>$payload['password']]);
}
src/Model/User/Projector/UserListProjector.php
55. src/Model/User/UserCommands.php
public function sendEmailConfirmationMessage(User $user, $email, $id)
{
$confirmationCode = $user->getEmailConfirmationToken();
$message = (new Swift_Message("Please confirm your email address"))
->addTo($email)
->addFrom($this->confirmationFrom)
->setBody("Please confirm your email with $id;$confirmationCode.")
;
$this->mailer->send($message);
return Event::NamedPayloadPairsToArray(User::SENT_CONFIRMATION);
}
56. features/bootstrap/UserContext.php
/**
* @When /^I follow the link in the confirmation email$/
*/
public function iFollowTheLink()
{
list($id, $token) = explode(';', $this->emailConfirmationToken);
$this->streamManager->recordCommand($id,
[$this->userCommands, 'confirmEmail'],
$token
);
}
57. src/Model/User/UserCommands.php
public function confirmEmail(User $user, $token)
{
if (!$user->matchEmailConfirmationToken($token)) {
return []; //Like nothing happened, for now we will ignore failed attempts
}
return Event::NamedPayloadPairsToArray(User::CONFIRMED_EMAIL);
}
In this example commands return events and throw errors. These things could also have been accomplished many other ways.
We usually need the payload. The ID is really only needed for the event that initiates a new aggregate root, but is passed to all of them in my implantation for consistency.
Projections that need to be transactionally included in a recording need to be in the same storage as the event store used.
The 1 here is the client generated ID. Normally this would be a UUID4. It is passed in twice, once as the first parameter to recordCommand() and again as the first parameter to the register command.
Constants (User::REGISTERED, etc) are the event names that will be recorded in the event store. The names of the constants can change to reflect the ubiquitous language, but the stored event names can’t be changed.
Assuming a unique constraint on the confirmed_email column this might throw an exception, the transaction will roll back and the application will have to deal with it. There is a better way that involves another aggregate root with email identifiers and associate / disassociate commands, but that would be too much to get into in a ½ hour talk!