domain-driven-designaggregates

Question about aggregate examples from red book


In the red book (Implementing Domain-Driven Design) Vernon Vaughn shows an aggregate example for a Scrum Core Domain. Unfortunately the code samples are just fragments.

For the second attempt the model is split in multiple aggregates instead of using a single large aggregate. As a result the method contract for the planBacklogItem method changes from

public class Product ... {
    ...
    public void planBacklogItem(
        String aSummary, String aCategory,
        BacklogItemType aType, StoryPoints aStoryPoints) {
            ...
    }
    ...
}

to

public class Product ... {
    ...
    public BacklogItem planBacklogItem(
        String aSummary, String aCategory,
        BacklogItemType aType, StoryPoints aStoryPoints) {
        ...
    }
}

The application service looks then like this:

public class ProductBacklogItemService ... {
    ...
    @Transactional
    public void planProductBacklogItem(
        String aTenantId, String aProductId,
        String aSummary, String aCategory,
        String aBacklogItemType, String aStoryPoints) {

      Product product =
            productRepository.productOfId(
                    new TenantId(aTenantId),
                    new ProductId(aProductId));

      BacklogItem plannedBacklogItem =
            product.planBacklogItem(
                    aSummary,
                    aCategory,
                    BacklogItemType.valueOf(aBacklogItemType),
                    StoryPoints.valueOf(aStoryPoints));

      backlogItemRepository.add(plannedBacklogItem);
    }
    ...
}

I understand the idea of aggregates and the sample code, but I wonder why planBacklogItem is not declared as static in the multiple aggregate version. The method does not access any instance data. Separate aggregates were used to achieve better concurrent access. Therefore I don't understand why the application service first pulls a full product from the repository, while none of its data is needed.

Since the BacklogItem uses Ids it can be created without reading the product from the repository. In case the product aggregate has lots of data and child aggregates are accessed frequently, the implementation can cause performance issues.

The only explanation I could come up with is that it should ensure the existence of the product. But why would you not use a method like productRepository.existsProduct(productId) instead?

I'm working with C# and don't understand all the magic of @Transactional. Vaughn did not say anything about the isolation level. Race conditions between reading the product and writing the BacklogItem could occur. Does @Transactional create a serializable transaction? I doubt it since not all storages support it. If the isolation level of the transaction is not serialized or the product is not locked during read it could be deleted before the BackLogItem was written. (It might not be a business case for the scrum examples, but it is a general concern).

I'm afraid that I miss something major. Thanks for any help.


Solution

  • The method does not access any instance data.

    Not sure if the book examples are incorrect, but it does use instance data on GitHub samples.

    public BacklogItem planBacklogItem(
                BacklogItemId aNewBacklogItemId,
                String aSummary,
                String aCategory,
                BacklogItemType aType,
                StoryPoints aStoryPoints) {
    
            BacklogItem backlogItem =
                new BacklogItem(
                        this.tenantId(), // <--
                        this.productId(), // <--
                        aNewBacklogItemId,
    

    If the isolation level of the transaction is not serialized or the product is not locked during read it could be deleted before the BackLogItem was written.

    Indeed, there could be a race condition (assuming we can remove products). TBH I don't know LevelDB enough and didint' dug into the actual implementation details, but a traditionnal foreing key constraint could prevent orphans in a relationnal DB.

    However, that wouldn't really work for logical deletes (e.g. archiving) so in these cases I guess there's those choices:

    1. Ignore the problem. Perhaps it doesn't actually matter that a backlog item gets planned a millisecond after a product has been archived? Race Conditions Don't exist?

    2. Lock the Product AR. That could be done in numerous ways, such as forcing an optimistic locking version bump.

    3. Apply an eventually consistent compensating action, such as unplanning a planned backlog item.