Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Timeseries/temporal data in DDD on write side in CQRS

I am having trouble getting my head around how I would support timeseries/temporal data in DDD and how it would be handled on the write side using CQRS. Ultimately I would like to find a solution that also plays nice with event sourcing.

Using temperature forecasts as an example, a change in temperature could also affect the forecast energy demand for a region/location. Assuming temperature forecasts can go far in to the future (based on historic data), loading all the forecasts in to a Location aggregate I think would be impractical without applying some limit to the amount of data loaded.

What is a good/recommended approach for synchronising/storing this kind of data to be used on the write side in CQRS when keeping event sourcing in mind?

Are any of my attempts below (Option A or B) considered as suitable DDD/CQRS solutions?

Option A:

Allow temperature to be updated independently and subscribe to events using a process manager/saga to then recalculate the demand. This solution would help keep aggregate size small, however it feels like the aggregate boundary could be wrong as demand is dependent on temperature and now spread across commands/events.

// OverrideTemperatureForecastCommandHandler.cs 
public void Handle(OverrideTemperatureForecast cmd)
{
    var from = cmd.TemperatureOverrides.Min(t => t.DateTime);
    var to = cmd.TemperatureOverrides.Max(t => t.DateTime);

    TemperatureForecasts forecasts = temperatureForecastRepository.GetByLocation(cmd.LocationId, from, to);

    forecasts.Override(cmd.TemperatureOverrides);

    temperatureForecastRepository.Save(forecasts);
    // raises 
    // TemperatureForecastsOverridden(locationId, overrides)
}

// TemperatureForecastsOverriddenProcessManager.cs 
public void Handle(TemperatureForecastsOverridden @event)
{
    var from = cmd.TemperatureOverrides.Min(t => t.DateTime);
    var to = cmd.TemperatureOverrides.Max(t => t.DateTime);

    // issue a command to recalculate the energy demand now temperature has changed...
    commandBus.Send(new RecalculateEnergyDemand 
       { 
          LocationId = @event.LocationId,
          From = from,
          To = to
       }));
}

// RecalculateEnergyDemandCommandHandler.cs 
public void Handle(RecalculateEnergyDemand cmd)
{
    EnergyDemand demandForecasts = energyDemandForecastRepository.GetByLocation(cmd.LocationId, cmd.From, cmd.To);

    // have to fetch temperature forecasts again...
    TemperatureForecasts temperatureForecasts = temperatureForecastRepository.GetByLocation(cmd.LocationId, cmd.From, cmd.To);

    demandForecasts.AdjustForTemperature(temperatureForecasts);

    energyDemandForecastRepository.Save(demandForecasts);
    // raises 
    // ForecastDemandChanged(locationId, demandforecasts)
}

Option B:

Create a single aggregate 'Location' and pre-load forecast data internally based on a given date range. This feels cleaner from a DDD behaviour perspective however loading an aggregate constrained to time range feels a bit awkward to me (or is it just me?). Without limiting the size of the forecasts values the 'Location' aggregate could get huge.

// OverrideTemperatureForecastCommandHandler.cs 
public void Handle(OverrideTemperatureForecast cmd)
{
    var from = cmd.TemperatureOverrides.Min(t => t.DateTime);
    var to = cmd.TemperatureOverrides.Max(t => t.DateTime);

    // use from/to to limit internally the range of temperature and demand forecasts that get loaded in to the aggregate.
    Location location = locationRepository.Get(cmd.LocationId, from, to);

    location.OverrideTemperatureForecasts(cmd.TemperatureOverrides);

    locationRepository.Save(forecasts);
    // raises 
    // TemperatureForecastsOverridden(locationId, overrides)
    // ForecastDemandChanged(locationId, demandforecasts)
}

For either option A or B, denormalisers on the read side could look something like:

// TemperatureDenormaliser.cs
public void Handle(TemperatureForecastsOverridden @event)
{
    var from = @event.Overrides.Min(t => t.DateTime);
    var to = @event.Overrides.Max(t => t.DateTime);

    var temperatureDTOs = storage.GetByLocation(@event.LocationId, from, to);

    // TODO ... (Add or update)

    storage.Save(temperatureDTOs);
}


// EnergyDemandDenormalizer.cs
public void Handle(ForecastDemandChanged @event)
{
    var from = @event.Overrides.Min(t => t.DateTime);
    var to = @event.Overrides.Max(t => t.DateTime);

    var demandDTOs = storage.GetByLocation(@event.LocationId, from, to);

    // TODO ... (Add or update)

    storage.Save(demandDTOs);
}
like image 383
Josef P. Avatar asked Nov 10 '22 17:11

Josef P.


1 Answers

Event sourcing would not be an option with either of your examples.

As new events come in, the older ones become irrelevant. These do not necessarily need to be in one aggregate; There are no invariants to protect for the whole history of readings.

Series of events could be managed in a saga instead, only keeping a limited amount of knowledge, and cascading into result events.

like image 92
istepaniuk Avatar answered Nov 15 '22 06:11

istepaniuk