EventsZilla: RavenDB modeling walkthrough

.NET, English posts, EventsZilla, RavenDB

Comments

7 min read
I needed a simple event publishing application. I also felt like doing another RavenDB sample app and a RavenDB post on it. This is how EventsZilla came to life. EventsZilla (full sources here: https://github.com/synhershko/eventszilla) is meant to be a simple web application to announce events along with a schedule, which is also capable of viewing past and future events. People should be able to register to an event without registering with the website, and also view slides and other content when it becomes available post-event. This post is being written during development, describing each stage and the considerations leading to the next. As such, the code I link to does not necessarily work, although it should. I will probably have some fixes and amendments made to the code after publishing this post.

Initial modeling

When we speak of an events publishing application, what are we looking at? The most basic items are an Event with means of registration, and a list of sessions for each Event. Each event should have a registration window, and a venue in which it takes place, and obviously title and description. For each session in an event we want to have a Presenter (possibly more than one), a title, a brief (aka abstract), and times in which each session starts and ends. We should note the start and end time of the event are going to be derived directly from the first and last sessions of the event. For now we call a session a "Schedule slot".

Unlike a relational model, with RavenDB we can sketch the entire thing as one class and just use it. There is one exception though - at this stage we already know venues and presenters might be showing several times in different events (maybe even the same presenter in multiple sessions in the same event), so we don't want to store them directly under the event, but rather link to them by storing their IDs only. They could be efficiently retrieved using the Includes feature.

We end up with this Event class:

[code lang="csharp"] public class Event { public Event() { Schedule = new List<ScheduleSlot>(); } public int Id { get; set; } public string Title { get; set; } public string Slug { get; set; } public string Description { get; set; } // markdown content public string VenueId { get; set; } public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset RegistrationOpens { get; set; } public DateTimeOffset RegistrationCloses { get; set; } public int AvailableSeats { get; set; } public class ScheduleSlot { public List<string> PresenterIds { get; set; } // list of person IDs public string Title { get; set; } public string Brief { get; set; } // markdown public DateTimeOffset StartingAt { get; set; } public DateTimeOffset EndingAt { get; set; } } public List<ScheduleSlot> Schedule { get; set; } public DateTimeOffset StartsAt { get { var firstSession = Schedule.OrderBy(x => x.StartingAt).FirstOrDefault(); return firstSession == null ? DateTimeOffset.MinValue : firstSession.StartingAt; } } public DateTimeOffset EndsAt { get { var lastSession = Schedule.OrderByDescending(x => x.EndingAt).FirstOrDefault(); return lastSession == null ? DateTimeOffset.MaxValue : lastSession.EndingAt; } } } [/code]

Since an event schedule has no meaning outside the scope of an event, it is best persisted there as well. It also means the whole schedule will be loaded with the event with each Load or Query operation this event will be part of. At this stage we are fine with that.

The StartsAt and EndsAt properties of the Event are persisted this way to take some pressure off the indexes we are going to create, so business logic will reside in the actual domain types instead of in the indexes as much as possible.

The Venue and Presenter classes are quite trivial ones, so won't be shown here.

The actual code for this phase is in this github commit.

Event registration

Registering to an event is quite a common operation in our system, and in a crowded website multiple registrations to the same event can be made at the same time. Like an event schedule, an event registration has no meaning at all outside the scope of the event itself, at least as long as we don't try and keep track of attendees (which we don't). Unlike the schedule, the attendees list is going to change quite a lot, and often at the same time. For this reason, keeping this list within the Event object itself won't make sense, as it will require us to start thinking about conflict resolution, when 2 or more people try to register for the same event on the same time. Another reason not to save registrations within the Event itself is we don't really care about it when we load an event, and that list can grow quite big for certain events. We want to make sure the Event object only holds data we are going to access frequently at that context; the registrants list is not that type of data. To keep the registrants list separate, while still making sure we don't need to worry about possible conflicts, I created a simple EventRegistration class which will hold of all that data. We persist it exactly that way, and whenever we need to know the amount of people who registered to the event, we query the DB for a count of registrations for that event. That query is using a simple static index we defined upfront. [code lang="csharp"] public class EventRegistration { public string EventId { get; set; } public string RegistrantEmail { get; set; } public string RegistrantName { get; set; } public DateTimeOffset RegisteredAt { get; set; } } [/code] Actual code for this phase is in this commit.

Available seats

Still in the context of registrations, it is important to note that by design we are not necessarily blocking registration for an event the second it is full. The reason for this is that we are getting the number of people that registered to the event through an index query, and while RavenDB's indexing process is quite fast, it is possible on a busy websites this query will return a number that is not entirely up to date. In EventsZilla, this is a design decision we made, not to think like a computer. Your PC knows to respect a hard limit, but in life, we hardly really do that. So if your event has 100 seats, wouldn't you be able to squeeze 10-15 more? and are you really that sure all of the original 100 registrants will indeed show up? For that reason we don't care if the count we got back from our query is not the actual count at the point of time where we issued the query. This is quite a common practice with Eventual Consistency - we don't try hard to respect hard limits that don't really exist anyhow, and wonderful things happen. RavenDB can tell us the count we got is stale. Waiting for non-stale results is possible, but in production is really not recommended, as it is going to significantly slow down your system. In some cases it can also result in an infinite wait. So don't do that. If we really needed to keep to a hard limit, we could add a RegistrantsCount property to our Event class, and increment it with every registration. It requires a bit more work to make sure concurrent writes are detected, and one is delayed, so no incorrect counts happen, but it will ensure we can know the exact count at all times, since we could then retrieve that event using a Load operation, which is ACID.

Next in line

As time allows, I will explore our possibilities for times when we want to enable different tracks in the event, and to better support events with only one session. Also in my to do list is letting each schedule slot have materials such as slides, video, code samples etc. Stay tuned.

Comments

Comments are now closed