I'm working on a project for a company that finds vendors to perform services for employee relocations. These services are things that movers don't have the expertise to do, like preparing a piano or for transit or constructing crates for valuables.
In this domain, an Order has 1:many Locations.
In the moving industry, orders are frequently in flux right up until the time the vendor is to perform the services requested of him. Therefore, in our model we have some statuses (e.g. Submitted, Cancelled, On Hold) that apply to Orders and Locations.
There's some pretty simple business rules that apply here. Here's a sampling:
And so on. From these rules, it seems obvious to me that this forms an aggregate root boundary. So therefore I have a MyClient.Statuses.Order
aggregate where Statuses
is the name of the context/service/whatever you want to call it:
public class Order {
private Guid _id;
private OrderStatus _status;
public void PlaceOnHold() {
if (_status == OrderStatus.Cancelled)
// throw exception
_status = OrderStatus.OnHold;
Locations.ForEach(loc => loc.PlaceOnHold());
}
public void PlaceLocationOnHold(Guid id) {
if (_status == OrderStatus.Cancelled)
// throw exception
Locations.Single(loc => loc.Id == id).PlaceOnHold();
}
// etc...
private Location[] Locations;
}
internal class Location {
public Guid Id;
public LocationStatus Status;
public void PlaceOnHold() {
// It's ok for a cancelled location on a non-cancelled order,
// but a Location cannot be placed On Hold if it's Cancelled so
// just ignore it
if (Status == LocationStatus.Cancelled)
return;
Status = LocationStatus.OnHold;
}
}
Both of these objects (Order, Location) are have GUID ids in other contexts (e.g. for CRUD based attributes that don't have state transitions). So now we finally get to my question:
I want to keep this thing DRY and Service-Oriented in order to minimize coupling, but it's really difficult to keep a parent-child relationship between two entities in only one place.
public class PlaceLocationOnHold_V1 {
public readonly Guid Id;
}
public class PlaceLocationOnHold_V1Handler {
public void Handle(PlaceLocationOnHold_V1 command) {
// This is typically a no-no. Should only fetch by OrderId:
var aggregate = _repository.GetByLocationId(command.Id);
aggregate.PlaceLocationOnHold(command.Id);
_repository.Save();
}
}
public class PlaceLocationOnHold_V2 {
public readonly Guid OrderId; // This feels redundant
public readonly Guid LocationId;
}
public class PlaceLocationOnHold_V2Handler {
public void Handle(PlaceLocationOnHold_V2 command) {
var aggregate = _repository.GetById(command.OrderId);
aggregate.PlaceLocationOnHold(command.LocationId);
_repository.Save();
}
}
public class LocationIdentity {
public Guid Id;
public Guid OrderId;
}
public class PlaceLocationOnHold_V3 {
public readonly LocationIdentity Location;
}
public class PlaceLocationOnHold_V3Handler {
public void Handle(PlaceLocationOnHold_V3 command) {
var aggregate = _repository.GetById(command.Location.OrderId);
aggregate.PlaceLocationOnHold(command.Location.Id);
_repository.Save();
}
}
Check out the Vaughn Vernon articles on Effective Aggregate Design. Specifically, Part 2 - there is some good information on modeling aggregates that communicate with each other.
The main point that is missing in your design is that these are both ARs as you have already mentioned - they are globally identifiable. So they should be referencing each other by ID. Order should not hold a child collection of Locations.
So your Order class will have a collection of LocationIds and your Location will have an OrderId.
public class Order
{
private Guid _id;
private OrderStatus _status;
private Guid[] _locationIds;
//...
}
public class Location
{
private Guid _id;
private Guid _orderId;
//...
}
Once you break that out correctly, Option #1 makes sense. Since Location is an AR unto itself, you can instantiate it and call PlaceOnHold directly on it without having to go through the Order AR.
As for the situation where a change in one AR trickles down into the others (i.e. placing Order on hold also makes all of the Locations go on hold as well), you could use domain events or eventual consistency.
public class Order
{
//... private instance variables
public void PlaceOnHold()
{
if (_status == OrderStatus.Cancelled)
// throw exception
_status == Orderstatus.OnHold;
DomainEvents.Handle(new OrderPlacedOnHold(_id)); // handle this, look up the related locations and call PlaceOnHold on each of them)
}
}
And for the situation where you may be trying to remove the hold on a Location, but the Order is on hold making the action illegal, you could instantiate the Order object in the command handler and pass it into the RemoveFromHold method of the Location. Vernon mentions this and reiterates the point that just because you can only ALTER one AR per transaction does not mean you cannot instantiate multiple ARs in the transaction.
public class RemoveHoldFromLocation : IHandler<RemoveHoldFromLocationCommand>
{
public void Execute(RemoveHoldFromLocationCommand cmd)
{
var location = locationRepo.Get(cmd.LocationId);
var order = orderRepo.Get(location.GetOrderId());
location.RemoveHold(order.GetStatus());
}
}
public class Location
{
//... private instance variables, etc.
public void RemoveHold(OrderStatus orderStatus)
{
if (orderStatus == OrderStatus.OnHold)
// throw Exception
_status == LocationStatus.OnHold;
}
}
This is just pseudocode so forgive typos, etc. Similar code samples are in the Vernon PDF.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With