Projections with mongo based on past events from different bounded contexes

Hello,

I’m trying to compose some projection based on stream of events from the event store.
In most cases the entry point, the insert, is clear and one event is responsible for it and all of the data that should be projected is ahead of it.
However sometimes i have data that has happened in the past and should be replicated across multiple projected documents.

Let me clarify by an example.

in order:

UserCreated(user_id,username)

WalletCreated(id,user_id)

The user service is not aware of wallet service.
They live on totally separate networks/devices, they have separate repositories.

I would like to know what is the username of owner of specific wallet, i could do that in few ways:

First, the user can provide query of username for specific id, since the wallet service knows an id of an user, i could dispatch two queries with axon, first would grab the wallet and second one would grab the username and return composed response.
This one looks handy at first but it has some problems, firstly i still want to get an username even if the user service becomes unavailable, secondly it’s two queries (performance :frowning: )

Second approach would be to create ‘book keeping’ repository inside the wallet projection that would record the usernames and the query would join two collections on the request just like the first approach but the joining would occur directly inside wallet service.

Third approach, similar to the previous one with one slight difference: the joining occurs on the projection. Since the wallet projection entry point knows an user id it can lookup the username immediately and save it in it’s own document so whenever query comes, it has all the data required.

Are there any other approaches to these kind of problems? Would really appreciate any points on that, because from my current understanding there is no clean way around that if i want to keep the projection of the second service unaffected by the first one, however i’m still thinking what would be the most performant solution for that.

Hi Robert,

You apparently have a query model that you want to expand based on past data from another (bounded) context upon a given event.
All three given options definitely work, but I understand none of them make you particularly happy.
Can say I have been dabbling this issue myself too a couple of times.

What we ended up doing, was the retrieval of the model from the other (bounded) context until it was actually queried for.
Thus a form of lazy initialization was used.

So, you wouldn’t immediately query (your first option), or use a ‘book keeping’ repo (your second option), or join the tables (your third option), as those are all on event ingestion time.
Instead, you would verify the query model whether it should contain the User Info once it’s queried, and then you’d retrieve the User Data if it isn’t present yet.
I would do this “last minute retrieval” through another query by the way, as it represent the “request for data” (the definition of a Query message).

That’s my two cents to the situation, hope this helps you out!

Cheers,
Steven

Hey Steven,

Thank you for your input, however i do not fully understand it, could you elaborate more on how exactly would you request for that data? Once the query of model A that requires model B data is handled the query requests B data through query message, so one query is merging two queries together?

Thanks

Hi Robert,

Sorry if my reply was a little hazy, should’ve made things clearer for you.
Practically speaking, I would have something likes this in code:

@QueryHandler
public Wallet handle(FindWalletQuery query) {
Wallet wallet = repository.find(query.getWalletId());
if (wallet.getUsername() == null) {
String username = queryGateway.query(new FindUsernameQuery(wallet.getUserId()), String.class)).join();
wallet.setUsername(username);
repository.save(wallet);
}
return wallet;
}

Thus, the query handler returning you the wallet has the smarts to check whether the username is contained in it.
If not, you query for the username, add it to the result and save it.
Then, you simply go back to returning the result upon handling the query.

As pointed out in my previous response, this is what we came up with.
There are ways to abstract this away from you (for example creating a ParameterResolver that resolves the username for you automatically), but in the end your Wallet Query Model apparently has the requirement to contain the Username.
Hence, it has “a request for data”, which simply is a query.
How and when you are going to dispatch that query is in the end up to you.

Hope this makes things clearer.

Cheers,
Steven

W dniu poniedziałek, 5 sierpnia 2019 10:47:30 UTC+2 użytkownik Steven van Beelen napisał:

@QueryHandler
public Wallet handle(FindWalletQuery query) {
Wallet wallet = repository.find(query.getWalletId());
if (wallet.getUsername() == null) {
String username = queryGateway.query(new FindUsernameQuery(wallet.getUserId()), String.class)).join();
wallet.setUsername(username);
repository.save(wallet);
}
return wallet;
}

Hey Steven,

Solution you provided is somewhat similar to the first approach of mine. However I see couple of problems there, the major one would be performance, because i would nest queries. Second problem would be query coupling (I’d rather have wallet context to have it’s own representation of a storage and not depend on the user context’s one, since if i consider future issues similar to it it might not be viable to query just the username from some bounded context read model)

I will try to follow the bookkeeping for now since it seems to be performant with little bit of overhead in the replication.

With that being said, thank you for your suggestions :slight_smile:

Cheers, Robert

Hi Robert,

The bookkeeping-repository approach is in essence not to different from sending a query if you ask me.
Both exemplify that you have the ‘request for data’ (aka a query), it’s just a different way of resolving this request for data.

However, modelling it as a distinct FindUsernameQuery makes the actual implementation of the Query Handler decoupled from the one requesting the data.
The FindUsernameQuery query handler might very well be your bookkeeping repository, as a simple key-value store is the fastest (thus most performant) solution.

Doing this makes the implementation more flexible, for things like scaling, separate deployments and performance requirements.

I wouldn’t see this as ‘query coupling’ to be a bad thing by the way.

Apparently your application/front-end has ‘the request for data’ to return a Wallet and the Username tied to it.

On face value that feels like to queries already.

In the end, it is your choice how far you go with modelling what your application does through service calls or through messaging.
A FindUsernameQuery to me however seems like a thing which would be requested more often, thus setting it up as such seems reasonable to me.

That’s my additional two cents. :wink:

Cheers,
Steven