Modelling your Domain in Sequent

A tour of the Post Aggregate

The app we generated in Getting Started comes with an example Post aggregate. In this guide we will take a quick look at Sequent’s directory structure, go over some of the concepts used in Sequent by expanding on Post and create our very own Author aggregate.

Directory structure

Let’s have a look at the general directory structure of a generated sequent project. If something doesn’t make sense right away, bear with us because we will walk through folders these one by one in the rest of this article.

We only need the publication_date attribute. Commands always target an aggregate, so we already know what to change by its aggregate_id. We could set a publish flag, but the event already communicates this intent.

Sequent retrieves the post for us and we call the (to be defined) publish method on the returned Post instance.

Learn all about command handlers in the CommandHandler Reference Guide.

The Aggregate Root

In lib/post/post.rb we find the aggregate root. This class encapsulates your business logic. Events are applied to instances of Post to give it its current state. We can see here that create a new Post will apply multiple events. Besides PostAdded we’re also applying events to change the author, title and content. You might be tempted to group all those fields in one event, which can be a good idea if those fields always change together. We’re using multiple events to emphasise that a single command does not always correlate to a single event.

Adding Author

So we have gone through the generated example. In order to add Author as an Aggregate we will need to make some changes to the Commands and Events. Since we want to ‘upgrade’ Author to an Aggregate we need to use the aggregate_id instead of a author String.

One of the things we need to do is to check the uniqueness of the Author’s email. Since we only store the events in the event store, we can not simply add a unique constraint to ensure uniqueness. A common solution to this problem is to create yet another Aggregate responsible for maintaining all usernames.
We will name this Aggregate Usernames. Since it needs to ensure uniqueness there can be only one instance of this, in order to achieve that we create this class as a Singleton.

In lib/usernames/usernames.rb add:

classUsernames<Sequent::AggregateRootclassUsernameAlreadyRegistered<StandardError;end# We can generate and hardcode the UUID since there is only one instanceID="85507d60-8645-4a8a-bdb8-3a9c86a0c635"defself.instance(id=ID)Sequent.configuration.aggregate_repository.load_aggregate(id)rescueSequent::Core::AggregateRepository::AggregateNotFoundusernames=Usernames.new(id)Sequent.aggregate_repository.add_aggregate(usernames)usernamesendend

We can now obtain the Usernames Aggregate by invoking Usernames.instance. Next thing we want to do is create a UserCommandHandler and add an Author. To ensure everything will work we start by defining our tests.

In spec/lib/author/author_command_handler_spec.rb

require_relative'../../spec_helper'require_relative'../../../lib/author'require_relative'../../../lib/usernames'describeAuthorCommandHandlerdobefore:eachdoSequent.configuration.command_handlers=[AuthorCommandHandler.new]endcontextAddAuthordoit'creates a user when valid input'it'fails if the username already exists'it'ignores case in usernames'endend

There might be more edge cases, but for now this is sufficient.

Let’s create the necessary classes in order to get the test to ‘green’.

We will stick to sequent’s suggested directory structure so we will end up with something like this:

Now when we run the tests all are marked as Pending: Not yet implemented. Before we can go any further we need to think about what kind of Events we are interested in. What do we want to know in this case? When registering our very first Author it will not only create the Author, but also create our Usernames Aggregate to ensure uniqueness of the usernames. So the test is something like:

When i add an Author for the first time
Then the Usernames registry is created
And the username is checked for uniqueness and added to the Usernames
And the Author is created with the given name and email

By leveraging Sequent’s test DSL we can create a test for this as follows:

In Sequent (or other event sourcing libraries) you test your code by checking the applied events.
In this case we modelled the AuthorNameSet and AuthorEmailSet as separate events since we they probably don’t change together. Also we can imagine to do different things when the email changes like sending a confirmation and such. You should take these considerations into account when moddeling your domain and defining your Events.

As you can see the events have no attributes yet. This is not necessary to make this test pass. Sequent only looks at the defined attributes and set those as values in the event. So you need to explicitly declare all attributes.

In lib/usernames/usernames.rb

classUsernames<Sequent::AggregateRootclassUsernameAlreadyRegistered<StandardError;end# We can generate and hardcode the UUID since there is only one instanceID="85507d60-8645-4a8a-bdb8-3a9c86a0c635"defself.instance(id=ID)Sequent.aggregate_repository.load_aggregate(id)rescueSequent::Core::AggregateRepository::AggregateNotFoundusernames=Usernames.new(id)Sequent.aggregate_repository.add_aggregate(usernames)usernamesenddefinitialize(id)super(id)applyUsernamesCreatedenddefadd(username)applyUsernameAdded,username: usernameendend

RuntimeError: cannot find aggregate type associated with creation event {UsernamesCreated: @aggregate_id=[85507d60-8645-4a8a-bdb8-3a9c86a0c635], @sequence_number=[1], @created_at=[2018-09-21T14:17:23+02:00]}, did you include an event handler in your aggregate for this event?

Sequent requires us to define an event handler in the Aggregate for at least the creation event, otherwise Sequent is not able to find an Aggregate in the repository.

So let’s change our aggregates to satisfy this demand.

Add to Usernames

classUsernames<Sequent::AggregateRoot...onUsernamesCreateddoendend

Add to Author

classAuthor<Sequent::AggregateRoot...onAuthorCreateddoendend

Running the spec again results in the following error: expected Usernames::UsernameAlreadyRegistered but nothing was raised

This is as expected since we didn’t implement anything. Let’s start by enforcing uniqueness.

And the lib/usernames/events.rb needs to contain the username attribute to make the test pass.

classUsernameAdded<Sequent::Eventattrsusername: Stringend

The event handlers UsernamesCreated and UsernameAdded will keep track of the current usernames in a Set. Whenever a new name is being added we first check if the name does not yet exist. If not then a new event is applied.

So let’s finish up by implementing our last test to ignore case when registering an Author.

Added a new Aggregate Author and Usernames and showed how Aggregate can depend on each other

Explored how to add tests in Sequent in order to test the domain

In this guide we mainly focussed on the domain. In the next guide we will take it a step further and see how can actually build a web application that our Authors can use. We will learn about how to initialize and setup Sequent with Sinatra, learn about Projectors and see how Sequent deals with migrations.