Created
April 26, 2011 21:39
-
-
Save masak/943237 to your computer and use it in GitHub Desktop.
Trying out CQRS and ES for a day
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Last Friday, Jonathan Worthington and I (Carl Mäsak) decided to get our feet | |
wet with CQRS and event sourcing. The toy project we settled on: a simple but | |
realistic web site for two-player board games. | |
In this post, I summarize how things went. | |
## Architect meets domain expert | |
Since there were only the two of us, I took the role of the domain expert, and | |
Jonathan took the role of the architect. He expertly teased a model out of | |
me. We arrived at two aggregate roots: `Player` and `Game`. Easy enough. | |
## Design: commands and events | |
Using an [inept but sufficient schema | |
tool](http://www.learnvisualstudio.net/content/videos/2501_walkthrough_creating_an_xsd_schema.aspx), | |
we drew up the [commands and | |
events](https://github.com/jnthn/hex/commit/e46c2c1a6fb5b5ed180867928441b558a37be3ff) | |
we figured we needed. These were as follows: | |
RegisterPlayerCommand PlayerRegisteredEvent | |
ActivatePlayerCommand PlayerActivatedEvent | |
InvitePlayerCommand PlayerInvitedEvent | |
AcceptInvitationCommand GameStartedEvent | |
RejectInvitationCommand InvitationRejectedEvent | |
PlaceStoneCommand StonePlacedEvent | |
GameWonEvent | |
SwapPlayerColorsCommand PlayerColorsSwappedEvent | |
ResignGameCommand GameResignedEvent | |
TimeOutGameCommand GameTimedOutEvent | |
For each command and event, we took a moment to model through what data we | |
needed to send along. It gave us an appreciation for one of the ways in which | |
commands and events differ: on the inside. | |
There was a moment of joyful insight as we realized that we had gotten this | |
far into the design of the system and not once talked about state. Quite a | |
refreshing change. | |
Being the one with the "domain expert" knowledge, I kept unwillingly slipping | |
back into the role of the client. Otherwise we'd have gotten some things wrong, | |
which wouldn't have shown up until the next "meeting with the client". | |
Jonathan remarked: "There's got to be a lesson in here somewhere." | |
(Afterwards, we've changed two things in the above model: we eventually realized | |
that we would need an InvitationAcceptedEvent after all. The reason we originally | |
figured we'd be able to do without it is that we noticed that it would fire off | |
a GameStartedEvent, and that would be enough. But no, it needs to fire off both, | |
otherwise the invitation would still be open. The other thing we realized was that | |
a better name for InvitePlayerCommand and PlayerInvitedEvent would be | |
MakeInvitationCommand and InvitationMadeEvent. That way, all the commands and events | |
contain in their names the aggregate that they are acting on -- which makes a lot | |
of sense.) | |
## Commands, events, test framework | |
We wrote our [first | |
test](https://github.com/jnthn/hex/blob/2e4b876537c1eb3c1459ccb19e11bbcc225048d3/t/gameplay.t), | |
and the necessary classes to go with it. | |
## Infrastructure | |
Our goal now was to get enough of the system running for our first test to | |
fail. That took a few hours, partly due to the fact that we were figuring out | |
how to fit everything together. | |
The wiring is like this: The test contains a 'given' list of events, a 'when' | |
command, and a 'then' list of events or an exception. The test fixture creates | |
an aggregate root to do the testing on, and loads it up with the events from | |
the 'given' part. Aggregate roots have [a special | |
flag](https://github.com/jnthn/hex/blob/master/lib/Hex/AggregateRoot.pm) on | |
the `apply_event` method for applying events without having them register as | |
changes to be committed. That's all the concession to testing that's | |
needed. Quite neat. | |
The test fixture then sends the command to a [bus-like | |
thing](https://github.com/jnthn/hex/blob/master/lib/Hex/EventAggregator.pm). This | |
triggers the right [command | |
handler](https://github.com/jnthn/hex/blob/master/lib/Hex/CommandHandlers/Game.pm#L32), | |
which does the required validation and then calls a [method on the | |
aggregate](https://github.com/jnthn/hex/blob/master/lib/Hex/AggregateRoot/Game.pm#L104). That's | |
the command part of things. | |
Now, the method on the aggregate is just a thin wrapper for [applying an | |
event](https://github.com/jnthn/hex/blob/master/lib/Hex/AggregateRoot.pm#L28). The | |
event is mapped through a [lookup | |
table](https://github.com/jnthn/hex/blob/master/lib/Hex/AggregateRoot/Game.pm#L48) | |
(our workaround for the lack of method overloading in Perl 5) to an | |
[apply-event | |
method](https://github.com/jnthn/hex/blob/master/lib/Hex/AggregateRoot/Game.pm#L72). Note | |
that on the way, we visited the same `apply_event` method as when we prepared | |
the aggregate with the 'given' events. This time the generated events *are* | |
saved, though... and that's exactly what we're then using to check against the | |
'then' events. (Or, if we got an exception, the test fixture captures that and | |
compares it with whatever was expected.) | |
It's quite a simple system, though it took us a few hours to understand and get | |
running. Still not too bad for our first attempt. | |
## Getting the first test to pass | |
Trying to get the test we'd written at the beginning of the day to pass, we | |
realized that we were still missing one component: a repository to store the | |
aggregate in while we were testing it. We settled on writing a [test | |
repository](https://github.com/jnthn/hex/blob/master/lib/TestRepository.pm), | |
with a total capacity of one (1) aggregate. | |
After that, things fell into place quickly. We got our event wired up, and | |
the test passing. Thus, we entered into the next phase... | |
## Ping-pong pair programming | |
By the looks of the commit log, that's where I became unconscious and Jonathan | |
kept on hacking. `:-)` But what really happened is that we paired up over | |
Jonathan's keyboard and started hacking in earnest. | |
Ordinary pair programming has a "driver" and a "navigator". In [ping-pong pair | |
programming](http://www.c2.com/cgi/wiki?PairProgrammingPingPongPattern), the idea | |
is for the two people to alternate by taking turns writing a test for the other | |
to implement. This was the first time we tried that, and it went very smoothly. | |
Definitely something to try again. In regular pair programming, the navigator | |
can sometimes doze off. But doing things this way, both of us were engaging | |
with the process of writing code and tests, even when we weren't in the role of | |
driver. | |
We got through eight such cycles of ping and pong. At this point, things were | |
really effortless: all the groundwork was already made, and now that we were | |
finally implementing state in our aggregates, there were no longer any | |
obstacles left. A very weird feeling; the aggregate was its own little world, | |
merely responding to commands and events as they came flying by. Coding was | |
effortless, not least because we managed to time it with the Ballmer Peak. | |
`:-)` Mmm, beer. | |
We surprised each other a bit by turning what appeared to be quite tricky | |
tests into excessively simple bits of implementation. Things generally | |
required *less* wiring up than we expected. (Again, because object state wasn't | |
the driving component, leaving us free to structure the innards of an aggregate | |
any which way we wanted.) | |
One thing we also discovered is that we generally had to write fewer tests than | |
"usual". Each new test covered a bit more ground than we expected, and we | |
often didn't bother to write a test because we already knew it was going to | |
pass. We're not sure whether that's (a) a good thing, and we shouldn't worry, | |
(b) a bad thing that's going to cost us in the future, or just (c) a sign that | |
we knew too much about the implementation. Guess more practice with this way | |
of testing will tell. | |
All in all, a happy first day with CQRS and event sourcing in actual practice. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment