22

EventSourcing works perfectly when we have particular unique EntityID but when I am trying to get information from eventStore other than particular EntityId i am having tough time.

I am using CQRS with EventSourcing. As part of event-sourcing we are storing the events in SQL table as columns(EntityID (uniqueKey),EventType,EventObject(eg. UserAdded)).

So while storing EventObject we are just serializing the DotNet object and storing it in SQL, So, All the details related to UserAdded event will be in xml format. My concern is I want to make sure the userName which is present in db Should be unique.

So, while making command of AddUser I have to query EventStore(sql db) whether the particular userName is already present in eventStore. So for doing that I need to serialize all the UserAdded/UserEdited events in Event store and check if requested username is present in eventStore.

But as part of CQRS commands are not allowed to query may be because of Race condition.

So, I tried before sending the AddUser command just query the eventStore and get all the UserNames by serializing all events(UserAdded) and fetch usernames and if requested username is unique then shoot command else throwing exception that userName already exist.

As with above approach ,we need to query entire db and we may have hundreds of thousands of events/day.So the execution of query/deserialization will take much time which will lead to performance issue.

I am looking for any better approach/suggestion for maintaining username Unique either by getting all userNames from eventStore or any other approach

James Z
  • 12,209
  • 10
  • 24
  • 44
Roshan
  • 873
  • 12
  • 33
  • 2
    As @Matt has stated in his answer, use an index of sorts on the command/write side. You could use something generic (haven't looked at NEventStore so I don't know if it has something similar): I have an `IKeyStore` interface in an experiment: https://github.com/Shuttle/shuttle-recall-core/blob/master/Shuttle.Recall.Core/IKeyStore.cs and you can see and implementation here: https://github.com/Shuttle/shuttle-recall-sqlserver/blob/master/Shuttle.Recall.SqlServer/KeyStore.cs --- A hash of an arbitrary key is associated with an AR id. Hope that helps :) – Eben Roux Jul 16 '15 at 05:52

4 Answers4

18

So, your client (the thing that issues the commands) should have full faith that the command it sends will be executed, and it must do this by ensuring that, before it sends the RegisterUserCommand, that no other user is registered with that email address. In other words, your client must perform the validation, not your domain or even the application services that surround the domain.

From http://cqrs.nu/Faq

This is a commonly occurring question since we're explicitly not performing cross-aggregate operations on the write side. We do, however, have a number of options:

Create a read-side of already allocated user names. Make the client query the read-side interactively as the user types in a name.

Create a reactive saga to flag down and inactivate accounts that were nevertheless created with a duplicate user name. (Whether by extreme coincidence or maliciously or because of a faulty client.)

If eventual consistency is not fast enough for you, consider adding a table on the write side, a small local read-side as it were, of already allocated names. Make the aggregate transaction include inserting into that table.

Matt
  • 1,648
  • 12
  • 22
  • 21
    The last paragraph is the pragmatic solution. – MikeSW Jul 13 '15 at 15:22
  • 2
    The last option is not feasible if the write side is an event store – Alexey Zimarev Oct 09 '15 at 11:17
  • 2
    @AlexeyZimarev there's nothing to stop you implementing a private read model which exists solely for the write model which isn't an event store, though. – Matt Oct 09 '15 at 11:45
  • I don't see the point. We say that the client should always issue a valid command. Although we can perform internal validations as many times as we want, the assumption is that at least uniqueness constraints are checked by the client. – Alexey Zimarev Oct 09 '15 at 12:45
  • 1
    @AlexeyZimarev I agree with you and I am not defending this strategy as more valid as any other. As with most things, the answer is 'it depends'. – Matt Oct 09 '15 at 13:05
  • About the last paragraph ("If eventual consistency..."): what if we don't support transactions (i.e. MongoDB)? – Constantin Galbenu Sep 21 '16 at 07:03
8

Querying different aggregates with a repository in a write operation as part of your business logic is not forbidden. You can do that in order to accept the command or reject it due to duplicate user by using some domain service (a cross-aggregate operation). Greg Young mentions this here: https://www.youtube.com/watch?v=LDW0QWie21s&t=24m55s

In normal scenarios you would just need to query all the UserCreated + UserEdited events. If you expect to have thousands of these events per day, maybe your events are bloated and you should design more atomically. For example, instead having a UserEdited event raised every time something happens on a user, consider having UserPersonalDetailsEdited and UserAccessInfoEdited or similar, where the fields that must be unique are treated differently from the rest of user fields. That way, querying all the UserCreated + UserAccessInfoEdited prior to accepting or not a command would be a lighter operation.

Personally I'd go with the following approach:

  1. More atomicity in events so that everything that touches fields that should be globally unique is described more explicitly (e.g: UserCreated, UserAccessInfoEdited)
  2. Have projections available in the write side in order to query them during a write operation. So for example I'd subscribe to all UserCreated and UserAccessInfoEdited events in order to keep a queryable "table" with all the unique fields (e.g: email).
  3. When a CreateUser command arrives to the domain, a domain service would query this email table and accept or reject the command.

This solution relies a bit on eventual consistency and there's a possibility where the query tells us that field has not been used and allows the command to succeed raising an event UserCreated when actually the projection hadn't been updated yet from a previous transaction, causing therefore the situation where there are 2 fields in the system that are not globally unique.

If you want to completely avoid these uncertain situations because your business can't really deal with eventual consistency my recommendation is to deal with this in your domain by explicitly modeling them as part of your ubiquitous language. For example you could model your aggregates differently since it's obvious that your aggregate User is not really your transactional boundary (i.e: it depends on others).

diegosasw
  • 13,734
  • 16
  • 95
  • 159
  • Hahah a few minutes later (33:24) one of the points he says if often asked: "All usernames must be unique".. it's funny coming from a Stackoverflow post to this video saying people ask it all the time – Ari Seyhun Oct 28 '21 at 16:28
5

As often, there's no right answer, only answers that fit your domain.

Are you in an environment that really requires immediate consistency ? What would be the odds of an identical user name being created between the moment uniqueness is checked by querying (say, at client side) and when the command is processed ? Would your domain experts tolerate, for instance, one out of 1 million user name conflict (that can be compensated afterwards) ? Will you have a million users in the first place ?

Even if immediate consistency is required, "user names should be unique"... in which scope ? A Company ? An OnlineStore ? A GameServerInstance ? Can you find the most restricted scope in which the uniqueness constraint must hold and make that scope the Aggregate Root from which to sprout a new user ? Why would the "replay all the UserAdded/UserEdited events" solution be bad after all, if the Aggregate Root makes these events small and simple ?

guillaume31
  • 13,738
  • 1
  • 32
  • 51
  • Hi @guillaume31, would there be a way to reach you outside SO? You seem pretty resourceful and there's a few modeling challenges I'd like to discuss with you if you don't mind? – plalx Jul 14 '15 at 13:51
  • @guillaume31 Yes,immediate consistency is required. – Roshan Jul 15 '15 at 11:51
  • @guillaume31 Yes,immediate consistency is required. UserName should be unique in application scope. Why would the "replay all the UserAdded/UserEdited events" solution be bad after all, if the Aggregate Root makes these events small and simple ? Ans: According to check uniqueness of userName I need all the userNames which exist. In eventstore we will be having lacs of events So , I need to replay all the events for all EntityId(Irrespective of my current EntityId). and for replaying all those may have performance impact Is there any other way in which can access all current active users? – Roshan Jul 15 '15 at 11:59
  • 1
    But can't you find a way to partition users so that the aggregate root that accepts a given user creation command only has to replay a reasonable number of events to check uniqueness ? – guillaume31 Jul 15 '15 at 12:16
  • You could try @Matt's solution from his last paragraph, but if you really have hundreds of thousands of user creations or user *name* modifications per day (which sounds absolutely astronomic to me), especially if they come in peaks, this will result in a lot of contention and transactional failures anyway. – guillaume31 Jul 15 '15 at 12:20
  • @RoshanG Don't know if that came across clear from my answer, but only UserAdded and User*Name*Modified would have to be replayed by this "scope" AR, not everything that could happen to a User (that would be handled by the `User` AR itself). – guillaume31 Jul 15 '15 at 14:00
  • @guillaume31 Thanks for your reply. But in this case we have to replay UserAdded/UserEdited events for all users. Being specific I do need some other entitieNames to be Unique which are being frequently Added/Edited. By the way I am trying with Matt's solution. – Roshan Jul 15 '15 at 14:22
  • @guillaume31 Thanks! I'm sorry, it's blocked from where I am right now. Are chats persistent and private? If I leave a message there later (e.g. email address) will you be able to pick it up? – plalx Jul 16 '15 at 12:19
3

With GetEventStore (from Greg Young) you can use whatever string as your aggregateId/StreamId. Use the username as the id of the aggregate instead of guids, or a combination like "mycompany.users.john" as the key and.. voila! You have for free user name uniqueness!

Narvalex
  • 1,824
  • 2
  • 18
  • 27
  • 1
    What if business demands the user name needs allow modifications? – Moim Nov 13 '17 at 20:52
  • @Moim You create another category of stream: "mycompany.usernames" In that category you store user names streams, like "mycompany.usernames-john" and store username binding events, like "UserId 1234 is now bounded to username "john" and another event like "UserId 1234 is not bounded to username "john" anymore and the username "john" is now free to take – Narvalex Nov 15 '17 at 11:39
  • @Narvalex it looks like EventStore hasn't had a CreateStream command for quite some time. You just write events to a stream key and if doesn't exist, it will be created. How do you prevent multiple UserCreated events to be appended to the same stream? Are you implying that you specify expectedVersion:-1 each time? – guillaume31 Mar 28 '18 at 11:26
  • Do you know the concept of "Hydration" or "Re-hydration" of an aggregate, right, @guillaume31 ? – Narvalex Apr 02 '18 at 18:56
  • @Narvalex Please be more explicit. Right now I don't see the relationship with my question. I was merely curious to know, on a technical level, how you get "free uniqueness" with EventStore. I guess it would be `expectedVersion:-1` on `AppendToStream()` to guarantee that a stream with same name doesn't exist, but not sure. – guillaume31 Apr 03 '18 at 17:29
  • Please excuse me, @guillaume31, my english is pretty bad. Let me try again. There are always some logic in between before comitting to the database. You should always expect some version to avoid concurrency stuff. Of course, if you want to append an event of type "BrandNewUserCreated" your expected version should be -1 on AppedToStream(). That is how you have free uniqueness, llike you said. But for Moim requirements (see the first comment) there might be and event of type "UserDeletedAccountAndLeftHisUsernameAvailable". With that you sould be alble to achive "UsenameTakenByOtherUser". – Narvalex Apr 04 '18 at 12:31