When I've had to implement CQRS/ES in real life, I've used a mongoDB data store for perisisting read models (aka projections) so my hands-on experience is there, but let's do some reasoning:
I can still have a DB with all entities, right? Or should the events be replayed every time the application is started to get the latest version of each entity in the memory? Seems like a waste on larger systems (as in large amount of data)?
In theory you can follow both approaches and still have a full fledged, correct implementation of CQRS/ES. The key of Event Sourcing is to use the operational data store to represent behaviours (sequence of domain events) over data (entity state) and, as postulate, events become the contract that tie together the aggregate[s] (read side) with their representations (read side). Nothing will constrain read-side implementation, except that it should behave consinstently with the events
Let's go through tradeoffs of both choices
- One solution for the read side is to attach event listeners that are used to update projections (what you've called "entities" but it's a misleading nomenclature here, since entities are on the other side of your implementation) and perform tasks that create/modify the projection and persist it in a DB storage. Here you can benefit of data durability and get rid of replaying events everytime the service is bootstrapped. Anyway, everytime you introduce breaking changes on your event you have to take care to develop mappers that let new event version
This approach fits well with situations where you have a large amount of events to process in your store and you don't want to replay them everytime to not hinder deploy time and to not cause datastore load spikes. Eventually though, your codebase will be full of patch mappers and you may want to get rid of them, so a replay will be still needed at some point
- In The second approach you should asses how big is the catalog of projections you are building. If you have 10M bills and each bills takes 100B of RAM, you've already made your memory footprint pretty big. Therefore, according with involved numbers, a countermeasure could be introducing an in-memory datastore (eg. Redis) which lets scale RAM avaliability and offer parallel access to muliple instances. Still, you have to replay events at every deployment or whenever data is not available in your datastore. This can be affordable only if you keep the event store pretty small, perhaps through continous snapshotting. The advantage is that you don't need to be aware of event versioning
so TLDR
- if you have few events / high development velocity -> in memory projection
- if you have a lot of events / high velocity -> DB projection with event mapping
- if you have a lot of events / low velocity -> DB projection with replay
- BONUS: if you need to make queries/aggregations on the projections -> DB