shumach5
shumach5

Reputation: 629

How to partially update aggregate in a DDD manner

I am developing an app with C# .NET in a DDD manner. I've also checked eshopcontainers, but it does not explain what I would like to know, so let me give a question here. My questions is

Detail
I am devloping a RSVP app. Now what I would like to do is sending reminder mail to people who has not voted yet. My domain model and Infrastructure layer is like this.

//Domain Model
class RSVP //RootAggreagate
{
    public long Id {get; private set;}
    public List<TimeSlot> TimeSlots {get; private set;}  

    public AutoRemindRule AutoRemindRule {get; private set;}
}

class AutoRemindRule 
{
    public long Id {get; private set;}
    public int IntervalHour { get; private set; }
    public DateTimeOffset NextTriggerDate { get; private set; }
    public DateTimeOffset RemindBeginDate { get; private set; }

    //Foreign Key for Plan
    public long RSVPId


    void SetNextTriggerDate() 
    {
        //Compute NewNextTriggerDate based on IntervalHour and RemindBeginDate field.
        NextTriggerDate = NewNextTriggerDate;
    }
}

//Infrastructure Layer (EF Core)
public class MyDbContext : DbContext
{
    //Plan Aggregate
    public DbSet<RSVP> RSVPs { get; set; }
    public DbSet<TimeSlot> TimeSlots { get; set; }
    public DbSet<AutoRemindRule> AutoRemindRules { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        ...
    }
}

In this model, I would like to run a batch that checks NextTriggerDate in AutoRemindRules periodically, and if NextTriggerDate is older than now, the batch sends mails to users who has not voted the RSVP. Eventually, the batch updates NextTriggerDate by calling SetNextTriggerDate. If I write a code like follows at Usecase(Application) layer in the batch, I can realize what I want. However, I do not think these codes follow the DDD rule because it updates the aggregate root partially. Is it ok to write a code like this in application layer, or if not, could anyone tell me a better way of coding?

//Usecase(Application) layer in a batch
using (var context = new MyDbContext) 
{
    var rules = context.AutoRemindRules.Where(i => i.NextTiggerdate < DateTimeOffset.Now);
    foreach (var rule in rule)
    {
        SendRemindMail();
        rule.SetNextTriggerDate();

        context.SaveChanges();
    }
}

Update
Another approach would be creating a method to update AutoRemindRule in the aggregate root, and the batch utilizes the method. However, my concern is performance and the load on DB. in that case, the batch has to read a bunch of unnecessary RSVP records in addition to AutoRemindRule records. I am wondering if there is another approach which is less load on DB while keeping the DDD manner.

Upvotes: 2

Views: 2448

Answers (1)

Francesc Castells
Francesc Castells

Reputation: 2847

Is it OK to partially update an root aggregate directly from batch operation?

The short answer is: No.

The point of aggregates is encapsulating data and exposing business operations so that the validations and invariants are checked and enforced. If you let external processes modify the data of the aggregates, then it completely defeats their purpose.

Potential solutions:

  1. Don't use Aggregates. If what you have is just a table with dates and some data and a batch job, but no or almost no business logic to enforce, then just do that: a table and a batch job. At first glance, it seems that the only thing you need to enforce is that the NextTriggerDate is sometime in the future and you can code that into the batch job. But I can imagine that you can have more rules, like don't trigger more than X times, don't trigger earlier than 1 day, etc. Or even let the aggregate decide the next trigger date, based on some internal logic (first trigger after 1 day, second after 2 days, etc). Aggregates are very handy for these things as each aggregate will store the state they'll need to calculate the next state change.

  2. Evaluate if it really is a problem to load the full RSVP aggregate or not. Loading some extra columns shouldn't be a big performance hit for most applications and a necessary price to pay in order to get the benefits of having the aggregate encapsulating its business logic in case you need to do things like the ones I mentioned on the previous point. If instead, you are building a feature with massive scale and little business logic, then consider a different approach.

  3. If the problem is that loading the full aggregate implies loading a big collection like the TimeSlots, that happens to be not necessary for that business operation, you could work around it by providing a specific method in your repository to load the aggregate without loading that collection. You should code your aggregate root operations that require that collection to validate if the collection was loaded and fail otherwise, to avoid bugs.

Upvotes: 3

Related Questions