0
votes

I'm trying to model online shop catalog using Domain Driven Design.

There are three main concepts I have right now: Product, Category, Attribute.

Attribute is a characteristic of a product. For instance things such as color, weight, number of CPU cores etc. There are attributes which possible values are fixed, for instance "condition" - can be new or used. Some of them are within some range of values, for instance "number of CPU cores". Some are freely created like "color".

Category have required attributes which every product within that category needs to have, and optional ones. Categories can have parent categories.

Product belongs to a single category which needs to be a leaf category(no children categories).

Now the problem I have is to model these three concepts as aggregates.

One option is to have three different aggregates: Product, Attribute, Category.

Product will have it's attribute values(each with parent id to Attribute AR). Attribute will be in different types(fixed, freely choosen, range). Category will have a list of IDs of Attributes which are required, and list of IDs

The issue here is that whenever I need to create a new product I would need to check if it has all of the required attributes, check the values, and then store the product. This validation would span three aggregates. Where should it go ? It should be domain service ?

Other option is to have 2 AR. Category, with it's products and Attributes. The issue here is again validation of correct values for a single attribute added to a product. The other huge issue I see here, is that I should fetch the whole aggregate from the repository. Given that category can have hundreds of products, I don't think that's a good idea. However it makes sense as a conceptual whole, as If I would like to delete a category, all of it's products should be deleted as well.

What I am missing here ?

2

2 Answers

1
votes

In "Implementing Domain Driven Design", Vaugh Vernon uses the "specification pattern" to handle entity/aggregate validation. Without quoting the entire chapter, you have different possibilities : (Java is used in my example, I hope you get the overall idea)

Validating Attributes / Properties

If it is a simple validation process field by field, then validate each attribute separately inside the setter method.

class Product {
    String name;

    public Product(String name) {
        setName(name);
    }

    public void setName(String name) {
       if(name == null) {
           throw new IllegalArgumentException("name cannot be null");
       }
       if(name.length() == 0) {
            throw new IllegalArgumentException("name cannot be empty");
       }
       this.name = name;
    }
}

Validating Whole Object

If you have to validate the whole object, you can use a kind of specification to help you. To avoid having the entity having too much responsibilities (dealing with the state, and validate it), you can use a Validator.

a. Create a generic Validator class, and implement it for your Product Validator. Use a NotificationHandler to deal with your validation error (exception, event, accumulating errors and then sending them ? up to you) :
public abstract class Validator {

    private ValidationNotificationHandler notificationHandler;

    public Validator(ValidationNotificationHandler aHandler) {
        super();

        this.setNotificationHandler(aHandler);
    }

    public abstract void validate();

    protected ValidationNotificationHandler notificationHandler() {
        return this.notificationHandler;
    }

    private void setNotificationHandler(ValidationNotificationHandler aHandler) {
        this.notificationHandler = aHandler;
    }
}

NotificationHandler is an interface, that you could implement given your requirements in term of validation error handling. Here is the interface proposed by Vaugh Vernon :

public interface ValidationNotificationHandler {

    public void handleError(String aNotificationMessage);

    public void handleError(String aNotification, Object anObject);

    public void handleInfo(String aNotificationMessage);

    public void handleInfo(String aNotification, Object anObject);

    public void handleWarning(String aNotificationMessage);

    public void handleWarning(String aNotification, Object anObject);
}

b. Implements this class with a specific validator ProductValidator:

public class ProductValidator extends Validator {
    private Product product;

    public ProductValidator(Product product, ValidationNotificationHandler aHandler) {
        super(aHandler);
        this.setProduct(product);
    }

    private void setProduct(Product product) {
        this.product = product;
    }

    @Override
    public void validate() {
        this.checkForCompletness();
    }

    private void checkForCompletness() {
        if(product.getName().equals("bad name") && anotherCondition()) {
            notificationHandler().handleError("This specific validation failed");
        }
        ...
    }
}

And then, you can update your entity, with a validate method, that will call this validator to validate the whole object:

public class Product {

    private String name;

    public Product(String name) {
        setName(name);
    }

    private void setName(String name) {
        if (name == null) {
            throw new IllegalArgumentException("Name cannot be null");
        }
        if (name.length() == 0) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
        this.name = name;
    }

// Here is the new method to validate your object
    public void validate(ValidationNotificationHandler aHandler) {
        (new ProductValidator(this, aHandler)).validate();
    }
}

Validating multiple aggregates

And finally, which is your direct concern, if you want to validate multiple aggregates to have something coherent, the recommendation is to create a Domain Service and a specific validator. The domain services can either have injected the repositories to look up for the different aggregates, or I everything is created by the application layers, then inject the different aggregates as method parameter:

public class ProductCategoryValidator extends Validator {
    private Product product;
    private Category category;

    public ProductCategoryValidator(Product product, Category category, ValidationNotificationHandler aHandler) {
        super(aHandler);
        this.setProduct(product);
        this.setCategory(category);
    }

    private void setCategory(Category category) {
        this.category = category;
    }

    private void setProduct(Product product) {
        this.product = product;
    }

    @Override
    public void validate() {
        this.checkForCompletness();
    }

    private void checkForCompletness() {
        // Count number of attributes, check for correctness...
    }
}

And the domain service that will call the Validator

public class ProductService {
    
    // Use this is you can pass the parameters from the client
    public void validateProductWithCategory(Product product, Category category, ValidationNotificationHandler handler) {
        (new ProductCategoryValidator(product, category, handler)).validate();
    }
    
    // Use This is you need to retrieve data from persistent layer
    private ProductRepository productRepository;
    private CategoryReposiory categoryReposiory;

    public ProductService(ProductRepository productRepository, CategoryReposiory categoryReposiory) {
        this.productRepository = productRepository;
        this.categoryReposiory = categoryReposiory;
    }
    
    public void validate(String productId, ValidationNotificationHandler handler) {
        Product product = productRepository.findById(productId);
        Category category = categoryReposiory.categoryOfProductId(productId);

        (new ProductCategoryValidator(product, category, handler)).validate();
    }
}

Like I said, I think you might be interested into the solution 3. As you have guessed it, you can use a Domain Service. But, add a specific validator to ensure the "responsibilities" are not mixed.

0
votes

The issue here is that whenever I need to create a new product I would need to check if it has all of the required attributes, check the values, and then store the product. This validation would span three aggregates. Where should it go ? It should be domain service ?

The usual answer is that the retrieval of information (aka I/O) is done in an application service. Copies of that information are then passed, like other inputs, into the domain model.

A single "transaction" might include multiple calls to aggregate methods, as we fetch inputs from different places.

These copies of information are generally treated as data on the outside - we have an unlocked copy of the data here; while we are using that copy, the authoritative copy might be changing.

If you find yourself thinking that "the authoritative copy of the data over there isn't allowed to change while I use it over here" - that's a big red flag that either (a) you don't actually understand your real data constraints or (b) that you've drawn your aggregate boundaries incorrectly.

Most data from the real world is data on the outside (Bob's billing address may change without asking your permission - what you have in your database is a cached copy of the Bob's billing address as of some point in the past).