Markus
Markus

Reputation: 71

DDD one transaction per aggregate - tracking case

Say I have two aggregates A and B. I'm using a factory method on A to create B. I also have a requirement that A cannot produce more than x instances of B.

It looks natural to have the following implementation:

A.createB() {

  if (total> x) 
        raise an error

  total++
  return new B()
}

But that would violate the rule of modifying two aggregates: creating B and modifying A.

If I try to comply to this rule, I would: 1. Create B in A and raise an event like BCreated. 2. Update A's total count in the next transaction by handling the BCreated event.

To me, in this particular example, this looks like a wierd workaround, since after calling the createB() method on A, I leave it in inconsistent state.

Am I missing something?

Upvotes: 1

Views: 146

Answers (3)

Eben Roux
Eben Roux

Reputation: 13256

Although the "one aggregate per transaction" is a rule it probably will not kill you to be pragmatic and ignore it in certain situations. In fact, I would argue that there are going to be cases where it just isn't practical or even possible to get by any other way.

That being said you definitely should do your utmost to stick to that guideline. Your case is not uncommon. Stock levels and airline tickets (also stock levels, really) come to mind, for instance.

The only way to split the operations into two distinct steps would be to track the process. For this you need a process manager and you may even need some messaging but that is all plumbing.

To get past the issue you would need to "reserve" the creation in the first step using, say, some correlation identifier. That could then be saved in transaction A:

    // begin tx (application layer)
    if (A.Reserve(id))
    {
        // we're good
        bus.Send(new RegisterBCommand
                 {
                    Id = id,
                    TheIdForA = theId
                    // other properties
                 }); // perhaps using Shuttle.Esb
    }
    // commit tx (application layer)

The next step would register the B entity and perhaps published BRegisteredEvent that could continue the process.

Just another point: you typically would only have A.CreateB() if both A and B live in the same bounded context. Another way to achieve something slightly similar would be by using an integration bounded context (say your orchestration BC) and then have CreateB() as an extension method on A where A and B are in separate BCs but the orchestration layer makes use of both domains. The other route is a plain factory or just new-ing it up in your application/domain service.

Upvotes: 0

VoiceOfUnreason
VoiceOfUnreason

Reputation: 57387

Am I missing something?

You aren't missing anything easy, no.

The general term for the problem you face is set validation -- if you are going to maintain an invariant over a set of data, then all operations that modify that data must pass through the same single lock.

When all the world is a single relational database, that lock may be implicit -- the database itself is processing all transactions in some (logically) serialized order, so with some care you can be certain that the invariant is maintained because, down at the storage level, each transaction is all or nothing.

But if you distribute that data among two databases, all bets are off.

Another way of thinking about it: if your transaction can only work when all of the different "aggregates" are stored in the same database, that's an indication that what you really have is a larger aggregate, implicit and hidden in your implementation details -- and it is going to be more expensive to scale.

Commonly, we can instead relax the invariant somewhat -- make a best effort at maintaining the invariant, but also detecting violations and defining a protocol to compensate.

Upvotes: 0

Sebastian Oliveri
Sebastian Oliveri

Reputation: 525

For the sake of simplicity I would treat those aggregate changes as a unit of work. The only advice is that you have to deal with race conditions. Now if you want to see an eventual solution as an example you will have to model an aggregate modeling the transaction of A and B changes: BCreation A.requestBCreation changes A state and emits an event (BCreationAllowed) BCreation reacts to and then BCreation dispatches a command to create B and handles its consequence domain event BCreated let's say, or BCreationRejected. Aggregate BCreation listens to any of these event and so on. It might be an complicated and overdesigned solution. You will also have to deal with race conditions and so 'syncronize' the process aggregate. Everything would be so much easier if you use the actor model.

Upvotes: 0

Related Questions