I'm trying to create a website that allows some persons to create events (title, date, time, etc.) and others to register for and also deregister from those events. The creator of the event can specify the maximum number of attendees (e.g. maximum 10 persons for a BBQ) (most of the time between 10 and 500 persons).
I also have some requirements (from "must have" to "nice to have"):
max number of attendees > current number of attendees
)I decided to give CosmosDB and its SQL API a try. I set up an "Event" container to store events in. Now I'm thinking about storing event registrations where I see the following possibilities, all with their advantages and disadvantages:
Include registrations within the event itself, e.g.
{
"id": "b21e28e9-61c6-454a-8438-4a75e74a854b",
"title": "BBQ",
"date": "2022-05-17",
"time": "17:00",
"maxAttendees": 10,
"attendeeIds": [
"5b5a6b75-4b4e-4824-8a7b-c1d9c7783357",
"1ad88926-037d-4bf0-b50c-b380f3f5fa9f",
]
}
maxAttendees
can be made transactional using the patch and conditional update (https://learn.microsoft.com/en-us/azure/cosmos-db/partial-document-update)attendeeIds
(again using the patch and conditional update to ensure that the index points to the correct item this might work, but it feels a bit dirty)Store registration events within the event itself (similar to an event store), e.g.
{
"id": "b21e28e9-61c6-454a-8438-4a75e74a854b",
"title": "BBQ",
"date": "2022-05-17",
"time": "17:00",
"maxAttendees": 10,
"attendanceEvents": [
{ "userId": "5b5a6b75-4b4e-4824-8a7b-c1d9c7783357", "type": "register" },
{ "userId": "1ad88926-037d-4bf0-b50c-b380f3f5fa9f", "type": "register" },
{ "userId": "5b5a6b75-4b4e-4824-8a7b-c1d9c7783357", "type": "deregister" },
{ "userId": "5b5a6b75-4b4e-4824-8a7b-c1d9c7783357", "type": "register" }
]
}
attendanceEvents
. Even when additionally storing the current number of attendees, we would need to ensure that two contiguous "register" events are only counted as one.Store registrations in another container (e.g. "EventRegistrations") (as with typical SQL DBs):
{
"id": "b21e28e9-61c6-454a-8438-4a75e74a854b",
"title": "BBQ",
"date": "2022-05-17",
"time": "17:00",
"maxAttendees": 10,
"currentAttendees": 3 // Optional, simplifies displaying available slots
}
{
"id": "6b5beddc-24be-4ddf-9171-7a680093870f", // optionally eventId and userId concatenated
"eventId": "b21e28e9-61c6-454a-8438-4a75e74a854b",
"userId": "5b5a6b75-4b4e-4824-8a7b-c1d9c7783357"
}
maxAttendees
and write the registration in one go (right?)Can anyone think of a way to mitigate the disadvantages of the above approaches? Or can anyone think of a totally different approach?
The first thing that pops up in my mind is to maybe think about the pattern: Command/Query
. This means you create two models (ReadModel
, WriteModel
), one is for the writing which contains all the data you need, and editing it should be very easy.
When you save the WriteModel
, you can make a distilled version called the ReadModel
and save it which will be prepared for rapid use. High index, flat structure. This will increase the performance very much.
Something like:
WriteModel
:
{
"id": "b21e28e9-61c6-454a-8438-4a75e74a854b",
"title": "BBQ",
"date": "2022-05-17",
"time": "17:00",
"maxAttendees": 10,
"attendanceEvents": [
{ "userId": "5b5a6b75-4b4e-4824-8a7b-c1d9c7783357", "type": "register" },
{ "userId": "1ad88926-037d-4bf0-b50c-b380f3f5fa9f", "type": "register" },
{ "userId": "5b5a6b75-4b4e-4824-8a7b-c1d9c7783357", "type": "deregister" },
{ "userId": "5b5a6b75-4b4e-4824-8a7b-c1d9c7783357", "type": "register" }
]
}
In the model (In my case C#) you can make all the validation
public class EventModel
{
public string Id {get;set;}
public string Title {get;set;}
public DateTime Date {get;set;}
public int MaxAttendees {get;set;}
public List<EventUserModel> EventUsers {get; private set;}
public void AddUser(string id, string type)
{
if (this.EventUsers.Count >= this.MaxAttendees) {
return;
}
this.EventUsers.Add(new EventUserModel() { Id = id, Type =type });
}
}
public class EventUserModel {
public string UserId {get;set;}
public string Type {get;set;}
}
And the ReadModel
will look like this:
public class EventReadModel
{
private int _currentAttendees;
public string Id {get;set;}
public string Title {get;set;}
public DateTime Date {get;set;}
public int MaxAttendees {get;set;}
public int CurrentAttendees
{
get => this._currentAttendees;
set => _currentAttendees = this.EventUsers.Count;
}
public List<EventUserReadModel> EventUsers {get; set;}
}
public class EventUserReadModel {
public string UserId {get;set;}
public string FirstName {get;set;}
public string LastName {get;set;}
public string Email {get;set;}
public string Type {get;set;}
}
The ReadModel
will be then used to query the data (Select) and will be updated every time you update the Model.
{
"id": "b21e28e9-61c6-454a-8438-4a75e74a854b",
"title": "BBQ",
"date": "2022-05-17",
"time": "17:00",
"maxAttendees": 10,
"currentAttendees": 3,
"attendanceEvents": [
{ "userId": "5b5a6b75-4b4e-4824-8a7b-c1d9c7783357", "type": "register", "firstName": ... },
{ "userId": "1ad88926-037d-4bf0-b50c-b380f3f5fa9f", "type": "register", "firstName": ... },
{ "userId": "5b5a6b75-4b4e-4824-8a7b-c1d9c7783357", "type": "deregister", "firstName": ... }
]
}
I hope that answers your question. Happy coding!