In an effort to save this question, I have re-written it. The original question was regarding how to associate aggregate roots. I have re-factored the code slightly but believe this may couple the code too tightly.

Do I introduce too tight of coupling by passing aggregate roots around? For example:

class Order
{
    public Order(int customerId, Address billing, Address shipping, List<Items> items)
    {
        _customerId = customerId;
        _billing = billing;
        _shipping = shipping
        _items = items;
    }

    private int _id;
    private Address _billing;
    private Address _shipping;
    private List<Item> _items;
}

class Customer
{
    public Order PlaceOrder(List<Item> items)
    {
        return new Order(_id, _billingAddress, _shippingAddress, items);
    }

    private int _id;
}

class Cart
{
    public Order Checkout(Customer customer)
    {
        var items = _items.ToList();
        _items.Clear();

        return customer.PlaceOrder(_items.ToList());
    }

    public AddItem(Product product, int quantity)
    {
        // Add item to cart
    }

    public RemoveItem(Product product, int quantity)
    {
        // Remove item from cart
    }

    private List<Item> _items = new List<Item>();
}

So then in my application service layer for a customer to checkout I would have something like:

public CustomerCheckoutService
{
    public void Checkout(int customerId)
    {
        var customer = _customerRepo.GetById(customerId);

        CompletePurchase(customer);
    }

    public void Checkout(int customerId, Address newShipping)
    {
        var customer = _customerRepo.GetById(customerId);

        customer.UpdateShipping(newShipping);

        CompletePurchase(customer);
    }

    private void MakePurchase(Customer customer)
    {
        var cart = _cartRepo.GetByCustomer(customerId);

        var order = cart.Checkout(customer);

        _orderRepo.Save(order);
        _cartRepo.Save(cart);
    }
}

I just feel like there is a lot of coupling between my aggregates with each one need each other.

  • Classic case of ‘down-vote and run’ – keelerjr12 Dec 5 at 12:11
  • They happen to be in in same bounded context, because they are supposed to be in same bounded context. Your understanding of what bounded context is and how it relates to aggregate roots is faulty. – Euphoric Dec 5 at 12:48
  • In this case I understand they are in the SAME BC. I also gave the example of a Shopper and Customer which are in different BCs. I understand what a BC is. My question relates to more of the how to associate aggregates with a 1:1 mapping. When a Customer/Shopper is created I also need to create the associated Cart. – keelerjr12 Dec 5 at 12:54
  • Let’s break this down further... Shopper and Cart are ARs in the Shopping BC. Cart has CartItems as part it’s Aggregate. In the Ordering BC, we have Customer and Order as ARs with Order consisting of OrderLines. When a shopper is created the associated Cart should also be created. Additionally when a Shopper checks out, this shopper needs to be associated with a customer so an order can be placed. This would be a better example that deals with associations between aggregates in different BCs as well as Aggregates in the same BC. – keelerjr12 Dec 5 at 12:58
  • It helps to have a strong definitions for shopper, customer, etc. – NoChance Dec 5 at 14:51

Your domain looks okay here except that we know a Cart does not CheckOut itself. Right? CheckOut represents more of a use-case than it does any one specific data mutation. "Checking out" is a process with which a Customer engages as a means to conduct some business transaction (PlaceOrder). As such, it makes more sense to keep this piece of knowledge in your application layer. Furthermore, often different ways/kinds of "checking out" begin to arise as a business develops.

Your service can be simplified to something like:

// CheckOutCommandHandler

purchaseRequest = purchaseRequests.Find( cmd.CustomerId )

customer = customers.Find( cmd.CustomerId )

order = customer.PlaceOrder( purchaseRequest ) // raise OrderPlaced

orders.save( order )

Given you have Cart and Customer in different bounded contexts, we do indeed need to introduce a new concept in your Order context that can hold the information necessary to PlaceOrder. In this case I've name it PurchaseRequest.

At it's core, I think your uncertainty is founded on some confusion regarding how the different entities you are working with relate to one another. Specifically, how your Shopping and Order contexts can share data with one another. This is resulting in the passage of more data around than necessary and a lot of double-dispatch (both forms of coupling).

It's critical to understand that your data model (your database) has no concept of "bounded contexts". This is makes sense because a database has no behavior (which is how bounded contexts are organized). The result of this is that a Cart and a PurchaseRequest are hydrated using the same data! The difference is that a Cart represents a Shopping context entity which is responsible for adding/removing items, and a PurchaseRequest represents an Order context value object that is copied into a new Order when placed. Additionally, it could be a place to hold an alternate shipping address, coupon codes, etc.

  • whoa... So you could have 2 different entities referencing the same table? Logically they're different, but physically the same? Mind-blown. What if you had a database/store for each BC (like commonly recommended)? The implementation would require that data be duplicated. Additionally, we have the invariant that when an Order is placed, the Cart should be emptied.. I assume you would handle this with a Domain Event such as CustomerOrderPlaced to which a Cart would subscribe? – keelerjr12 Dec 6 at 20:53
  • I think our issue is that we are trying to map our domain models to our database tables 1:1 and it's really causing confusion in our implementation. But in this case a CartItem and PurchaseOrder could be mapped to the same LineItem table. – keelerjr12 Dec 6 at 20:55
  • @keelerjr12 It's true that it's usually not a good idea to "share" data between entities, but this is a very specific case regarding our Cart and PurchaseRequest. They aren't sharing so much as copying. Think of it like this, any other implementation is going to look something like: pr = new PurchaseRequest( cart.ToList() ) anyway (which is essentially the same thing as hydration from the DB with more coupling). Somehow someway, the contents of the Cart need to be retrieved and copied into an Order. This simply must occur. – king-side-slide Dec 6 at 21:04
  • @keelerjr12 Hydrating different Entities with the same data is totally valid provided only one of those Entities holds invariants and allows for the data to be changed. Conceptualizing Value Objects for message passing is not only extremely useful, it is sometimes necessary. And yes, use your OrderPlaced event to empty the Cart. – king-side-slide Dec 6 at 21:09
  • 1
    @keelerjr12 Well, we aren't creating an Aggregate Root! In any case, don't take his advice to be gospel. You have to design your system. If for some reason, you would rather query your Cart and have it expose it's List to a VO, then go for it. Prioritize quantity over quality. Once you have a feel for how things should look/work, you will find the former results in more quality anyway. – king-side-slide Dec 6 at 21:29

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.