Aggregate state management

I would be interested to hear how other people go about writing and reading aggregate state with Eventstore.

The process we use is:

  1. Update our C# Aggregate with the required changes

  2. Aggregate creates a list of C# class events

  3. C# events gets written to eventstore

  4. When we need to rehydrate the aggregate, we retrieve all the json events from Eventstore

  5. C# then deserialise back to each C# event class and applies to the Aggregate

There are two things that trouble me with the approach:

  1. We have to return all the events (outwith snapshots), and these events tend to have duplicate fields (customer indentifiers etc)

For example:

{ “organisationId” : 1, “lineTotal” : 1.45 }
{ “organisationId” : 1, “lineTotal” : 0.99 }

``

  1. When we write the events, we have to include “optional” fields.

By this, if we have a C# event that has a field only uses in certain scenarios, the field would always be written out in json (with the default value)

For example:

{
“ageCheck” : false
}

``

This field (in our system we almost always be false, so it seems a waste to always write this (but, it really does need to be part of the event when it’s true, rather than a new event)

So, how does this sound:

Our Aggregate can use annoymous types for creating the events:

public void AddSaleLine(Decimal lineTotal)
{
//Only include what we want
var saleLine = new {
type = “saleLine” // no assembly type needed
lineTotal
}

var @event = JsonConvert.Serialise(saleLine);

}

``

public void AddSaleLineWithAgeCheck(Decimal lineTotal)
{
//Only include what we want
var saleLine = new {
type = “saleLine” // no assembly type needed
lineTotal,
ageCheck = true
}

var @event = JsonConvert.Serialise(saleLine);

}

``

When we need to rehdyrate our Aggregate, rather than bringing back all the events, we simply return the state (and deserialise it to our aggregate)

The state could be maintained either by:

  1. Transient query

  2. Partition

The state would hold the version number in for concurrency for future writes.

Steven

Why do you want to event source?

Many would argue that where you ended up is not event sourcing but more a continuous snapshot with no capture of why something is changing.

If that meets your needs then great but it is not eventsourcing imo.

A normal process would be

  1. read events

  2. apply events to aggregate to get to current state

  3. pass a command into the aggregate and get one or more events back (or exception maybe)

  4. write events.

Examples of event are

Product Created

Item Added

Age Check Passed

Age Check Failed

OrganisationAddedLineTotalForSomeReason

They are not simply this row on this table changed somehow.

So their names and types are important - so an event cannot have an optional field. If it does then you are dealing with at least 2 events.

With events you could use the kind of approach you describe - but is c# really the language you want if you are doing this? Think about deserialisation.

Re dupplicate field: are they not in a stream named after aggregate? Adding the id will be useful for readmodels though.

Also is it really duplicate data? It’s the same but each event is a seperate piece of behaviour or slice of the system. The data in an event is dictated by its consumers (IE policies and read models). How these things consume and work is not the aggregates concern. It’s job is simply turning commands into events - not the why of the data on the events.

There is a lot of info out there - look at event modelling of any of that seems odd.

Long, sorry! Hopefully someone will correct any errors!

John

John,

Thanks for replying.

I think my solution I posted is still event sourced.

I still need to replay the events to get to my state, it’s just rather than brining back events and iterating round them in my C#, I would let the EventStore do this for me?

In my head, that is still replaying the events, and would always give you the latest version of the aggregate (unlike reading the state froma read model that might be stale)

Using your example, our aggregates would still have the same events replayed to get to the current state:

  • Product Created
  • Item Added
  • Age Check Passed
  • Age Check Failed
  • OrganisationAddedLineTotalForSomeReason

it’s just my proposal would happen at the server side.

On your comment about optional fields. This part I don’t know what I want exactly.

In my my example, we have a sale line, that might have had an age check performed.

What’s important though, is this is a sale line, rather than a different event (i.e. subscribers listen for sale lines only, not sales lines + slightly different variants)

Currently, we need to include “ageCheck” in all the events, even though its rarely used.

The duplicate data, the read model side, is pretty important, which is why we have organisationId (and various other fields) in each event. It’s used to identifiying databases to use.

So on the most part, a single stream in our system will belong to a single organisation, so it would be repeated.

For our Sales, the organsiation Id is not part of the stream name (maybe it should), so potentially the “duplicate” data would be part of a stream name (would keep this limited though) and when we delivery events to subscribers, we simply enrich the event with the “duplicate” data then.

John,

I am starting to doubt myself now.

Here are two examples that up until recently I would have classes as the same event type:

{
“type”: “saleTransactionLineAddedEvent”,
“lineDateTime”: “05/03/2020 12:05:08”,
“lineNumber”: 2,
“barcode”: “3501170815038”,
“quantity”: 1.0,
“lineTotal”: 3.0,
“retailPricePerUnit”: 3.0,
“vatRate”: 0.2,
“costPerUnit”: 1.0
}

``

{
“type”: “saleTransactionLineAddedEvent”,
“lineDateTime”: “05/03/2020 12:05:08”,
“lineNumber”: 1,
“barcode”: “3501170815038”,
“quantity”: 1.0,
“lineTotal”: 0.0,
“retailPricePerUnit”: 3.0,
“vatRate”: 0.2,
“costPerUnit”: 1.0,
"reason": "Price Enquiry"
}

``

In the second event, “reason” field has been included.

i guess this is where I am having the dilema.

Are these really the same event type, or just as you say, they are actually different.

Sorry to jump in here (I'm John L)

I would argue you have the same event here, but a newer version. This is a common problem in event sourcing, when you need to capture something in an event that you haven't been previously. You can either create a new version of the event and "upgrade" older events as you replay or if things are so different, create a new event. In your case of just adding a reason field, I would just do a new version and assume reason is null for all your old events as you replay.

John L,

Yeah, that’s kinda where I was going with this.

The only thing thats slightly different, is both “versions” would exist. The version witht he optional field isn’t intended to replace the original.

On my original post, if I had this:

{
“lineTotal” : 0,
“reason” : null
}

``

and:

{
“lineTotal” : 0,
“reason” : “Price Override”
}

``

In my head, these events are just the same as the version with the omitted “reason”