Multiple Aggregates/Repositories in One Transaction

Multiple Aggregates / Repositories in one Transaction

I think what you really meant to ask was regarding 'Multiple Aggregates in one transaction'. I don't believe there is anything wrong with using multiple repositories to fetch data in a transaction. Often during a transaction an aggregate will need information from other aggregates in order to make a decision on whether to, or how to, change state. That's fine. It is, however, the modifying of state on multiple aggregates within one transaction that is deemed undesirable, and I think this what your referenced quote was trying to imply.

The reason this is undesirable is because of concurrency. As well as protecting the in-variants within it's boundary, each aggregate should be protected from concurrent transactions. e.g. two users making a change to an aggregate at the same time.

This protection is typically achieved by having a version/timestamp on the aggregates' DB table. When the aggregate is saved, a comparison is made of the version being saved and the version currently stored in the db (which may now be different from when the transaction started). If they don't match an exception is raised.

It basically boils down to this: In a collaborative system (many users making many transactions), the more aggregates that are modified in a single transaction will result in an increase of concurrency exceptions.

The exact same thing is true if your aggregate is too large & offers many state changing methods; multiple users can only modify the aggregate one at a time. By designing small aggregates that are modified in isolation in a transaction reduces concurrency collisions.

Vaughn Vernon has done an excellent job explaining this in his 3 part article.

However, this is just a guiding principle and there will be exceptions where more than one aggregate will need to be modified. The fact that you are considering whether the transaction/use case could be re-factored to only modify one aggregate is a good thing.

Having thought about your example, I cannot think of a way of designing it to a single aggregate that fulfills the requirements of the transaction/use case. A payment needs to be created, and the coupon needs to be updated to indicate that it is no longer valid.

But when really analysing the potential concurrency issues with this transaction, I don't think there would ever actually be a collision on the gift coupon aggregate. They are only ever created (issued) then used for payment. There are no other state changing operations in between. Therefore in this instance we don't need to be concerned about that fact we are modifying both the payment/order & gift coupon aggregate.

Below is what I quickly came up with as a possible way of modelling it

  • I couldn't see how payments make sense without an order aggregate that the payment(s) belong to, so I introduced one.
  • Orders are made up of payments. A payment can be made with gift coupons. You could create other types of payments, such as CashPayment or CreditCardPayment for example.
  • To make a gift coupon payment, the coupon aggregates must be passed to the order aggregate. This then marks the coupon as used.
  • At the end of the transaction, the order aggregate is saved with its new payment(s), and any gift coupon used is also saved.

Code:

public class PaymentApplicationService
{
public void PayForOrderWithGiftCoupons(PayForOrderWithGiftCouponsCommand command)
{
using (IUnitOfWork unitOfWork = UnitOfWorkFactory.Create())
{
Order order = _orderRepository.GetById(command.OrderId);

List<GiftCoupon> coupons = new List<GiftCoupon>();

foreach(Guid couponId in command.CouponIds)
coupons.Add(_giftCouponRepository.GetById(couponId));

order.MakePaymentWithGiftCoupons(coupons);

_orderRepository.Save(order);

foreach(GiftCoupon coupon in coupons)
_giftCouponRepository.Save(coupon);
}
}
}

public class Order : IAggregateRoot
{
private readonly Guid _orderId;
private readonly List<Payment> _payments = new List<Payment>();

public Guid OrderId
{
get { return _orderId;}
}

public void MakePaymentWithGiftCoupons(List<GiftCoupon> coupons)
{
foreach(GiftCoupon coupon in coupons)
{
if (!coupon.IsValid)
throw new Exception("Coupon is no longer valid");

coupon.UseForPaymentOnOrder(this);
_payments.Add(new GiftCouponPayment(Guid.NewGuid(), DateTime.Now, coupon));
}
}
}

public abstract class Payment : IEntity
{
private readonly Guid _paymentId;
private readonly DateTime _paymentDate;

public Guid PaymentId { get { return _paymentId; } }

public DateTime PaymentDate { get { return _paymentDate; } }

public abstract decimal Amount { get; }

public Payment(Guid paymentId, DateTime paymentDate)
{
_paymentId = paymentId;
_paymentDate = paymentDate;
}
}

public class GiftCouponPayment : Payment
{
private readonly Guid _couponId;
private readonly decimal _amount;

public override decimal Amount
{
get { return _amount; }
}

public GiftCouponPayment(Guid paymentId, DateTime paymentDate, GiftCoupon coupon)
: base(paymentId, paymentDate)
{
if (!coupon.IsValid)
throw new Exception("Coupon is no longer valid");

_couponId = coupon.GiftCouponId;
_amount = coupon.Value;
}
}

public class GiftCoupon : IAggregateRoot
{
private Guid _giftCouponId;
private decimal _value;
private DateTime _issuedDate;
private Guid _orderIdUsedFor;
private DateTime _usedDate;

public Guid GiftCouponId
{
get { return _giftCouponId; }
}

public decimal Value
{
get { return _value; }
}

public DateTime IssuedDate
{
get { return _issuedDate; }
}

public bool IsValid
{
get { return (_usedDate == default(DateTime)); }
}

public void UseForPaymentOnOrder(Order order)
{
_usedDate = DateTime.Now;
_orderIdUsedFor = order.OrderId;
}
}

Multiple repositories under the same transaction

Repositories don't know about the fact that they are running under some transaction, or should they?

Here's what Evan's wrote, in the Blue Book

The REPOSITORY concept is adaptable to many situations. The possibilities of implementation are so diverse that I can only list some concerns to keep in mind....
Leave transaction control to the client. Although the REPOSITORY will insert and delete from the database, it will ordinarily not commit anything.... Transaction management will be simpler if the REPOSITORY keeps its hands off.

To be absolutely honest, I find this notion difficult to reconcile with other writings from the same chapter.

For each type of object that needs global access, create ( a REPOSITORY ) that can provide the illusion of an in-memory collection of all objects of that set.

In the languages that I normally use, in-memory collections typically manage their own state. Doesn't that imply that transaction control should be behind the collection interface, rather than left to the client?

An additional point is that our persistence solutions have gotten a lot more complex; for Evans, the domain model was only managing writes to a single database, which supported transactions that could span multiple aggregates (ie, an RDBMS). But if you change that assumption, then a lot of things start to become more complicated.

cqrs offers some hints, here. Reading from repositories is beautiful; you take all of the complexity of the implementation and hide it behind some persistence agnostic facade. Writing can be trickier -- if your domain model needs support for saving multiple aggregates together (same transaction), then your "repository" interface should make that explicit.

These days, you are likely to see more support for the idea that modifications of aggregates should always happen in separate transactions. That takes some of the pressure off of repository coordination.

DDD: creating multiple aggregates with a shared life-cycle in a single transaction

As pointed out by @plalx, contention doesn't matter as much when creating aggregates in terms of transactions, since they don't yet exist so can't be involved in contention.

As for enforcing the mutual life cycle of multiple aggregates in the domain, I've come to think that this is the responsibility of the application layer (i.e. an application service, or use case).

Maybe my thinking is closer to Clean or Hexagonal architecture, but I don't think it's possible or even sensible to try and push every single business rule down into the "domain model". The point of the domain model for me is to partition the problem domain into small chunks (aggregates), which encapsulate common business data/operations that change together, but it's the application layer's responsibility to use these aggregates properly in order to achieve the business' end goal (which is the application as a whole), including mediating operations between the aggregates and controlling their life cycles.

As such, I think this stuff belongs in an application service. That being said, frequently updating multiple aggregates in each use case could be a sign of incorrect domain boundaries.

Create multiple aggregates in one transaction

You should probably treat the recording of the receipt event and generation of the five pallets as two separate transactions.

The changes are across two aggregates in your case, but your application service should ideally deal with one single aggregate, as much as possible. Domain Events are the right structures to solve such problems of extended transactions, be it between aggregates in a single BC, or across BCs.

So your application service would record the receipt of five pallets as a transaction, and bubble up a domain event (say PalletsUnloaded) with sufficient context and data. The event would be passed on as a data structure to a message broker, to be retrieved by subscribers registered for the domain event.

The Pallet aggregate would then catch the event through an event-specific subscriber and process it, in one of two ways:

  1. You can create the five pallets in one single pass. Transaction-wise, this approach is a bit risky because if you are using files as persistent storage, there may be failures and you may not have granular data to identify the exact issue
  2. You catch the event and create five separate event messages (say CreatePallet), each of which is submitted back to the message broker. A subscriber for this event would pick them up and create pallet records one by one. You will accurately know which one failed and why

The 2nd approach is also safer because if you use reliable message brokers like RabbitMQ as the transfer mechanism for events, you can send the errored event into a dead-letter queue or set up mechanisms to retry the processing later. You can also build a separate error-handling process/view that deals with, and processes, errored events.

Multiple Aggregates Root INSTANCES per transaction

I think an AR may be more about the consistency boundary than the transaction boundary.

The fact that a transaction happens to fit nicely around an AR boundary is just coincidence.

Since transactions are more of an application layer concern, if you end up with more than one AR in a transaction then it does not necessarily indicate a design issue.

In fact, I would go so far as to say that in some 100% consistency requirement scenarios you may not even have a choice but to include all changes in one transaction.

Can I update multiple aggregate instances of the same type in one transaction?

The type of AR is irrelevant, ARs are always transactional boundaries. When working with many of them you should usually embrace partial failures and eventual consistency.

Why should all renames fail or succeed if there are no invariants to protect across all these ARs. For instance, if the UI did allow users to rename multiple products at a time and some user renames hundreds of them. If one product failed to be renamed because of a concurrency conflict for instance, would it be better not to rename any of the products or inform the user that one of them failed?

You can always violate the rule, but you must understand that the more ARs that are involved in a transaction, the more likely you are to experience transactional failures due to concurrency conflicts (assuming contention can occur).

I usually only modify multiple ARs in one transaction when there are invariants crossing ARs boundaries and I can't afford to avoid dealing with eventual consistency.

Multiple aggregates handle with one table?

In general, DDD is all about not polluting the domain with infrastructure concerns; whether different aggregates are stored in the same table is an infrastructure concern. As long as the repository/repositories are able to meet their obligations, go for it.

That said, having an Order have a lot of variation in terms of what operations are legal and what information is available from state to state might be a sign that the states might make sense being apportioned to different bounded contexts (e.g. a context where items are added to an order (e.g. a cart context), a checkout/payment context, an assembly for delivery context, and a being delivered context).

Modifying two aggregates in a single transaction (DDD)

You should resist the temptation to modify two aggregates in a single transaction. There are two rules that help you to not do that:

  1. Reference Other Aggregates Only by Their ID
  2. Use Eventual Consistency Outside the Boundary

So, in order to update the AssignedUsage for a GearItem you will use eventual consistency.

You could do that using domain events or by periodically calculating the usages for all GearItems.

Using domain events:

After a GearItem is assigned to an Activity, you publish a GearItemAssignedToActivity event that is caught by a Process manager that calls GearItem.AssignUsage(Time,Distance) in a new transaction (by first loading the GearItem using its ID from the Repository).

Periodically updating the GearItem's usage

In a cron/scheduled task/whatever you reset the usage to each GearItem, then load every Activity and load and call GearItem.AssignUsage(Time,Distance) for each assigned GearItem.

Also, I suggest that you decouple the two aggregates and do not pass references to method calls. For example, you should not pass the entire Activity aggregate to the GearItem.AssignUsage method, but only the required properties. In this way a GearItem must not know or depend on the entire Activity.



Related Topics



Leave a reply



Submit