1

I'm not sure where to place the following checks I have now in the material domain class. The issue is, that I need to validate that material exists and is valid in external database. Only after that validation I can be sure that I can use this material in our new project and store it in database. The Material object is part of collection in Order aggregate object. This is simplified example to demonstrate the issue.

Material class

  public class Material
        {
            public int? Id { get; private set; }
            public int RequestId { get; private set; }
            public int MaterialNumber { get; private set; }
            public int Quantity { get; private set; }

            // EF
            private Material()
            {
            }

            public Material(int requestId, int materialNumber, int quantity,
                string materialType, string materialStatus, string materialCondition)
            {
                if (materialCondition != "ZPR0")
                {
                    throw new InvalidOperationException("Material condition is not valid.");
                }

                if (materialType != "FERT" || materialType != "UNBW")
                {
                    throw new InvalidOperationException("Material type is not valid.");
                }

                if (materialStatus != "Z2" || materialStatus != "Z3" || materialStatus != "Z4")
                {
                    throw new InvalidOperationException("Material status is not valid.");
                }

                Id = requestId;
                MaterialNumber = materialNumber;
                Quantity = quantity;
            }
        }

Order class

public class Order : IAggregate
    {
        public int? Id { get; private set; }

        public IList<Material> Materials { get; }

        // EF
        private Order()
        {
        }

        public AddMaterial(Material material)
        {
            Materials.Add(material);
        }
}

Should I load materialType, materialStatus and materialCondition values from the database and pass it into domain entity inside Application layer (API) ? Or should I call some domain service (or repository method inside application layer) or domain validator to call some method IsMaterialValid(materialType, materialStatus and materialCondition) and after that materialize the Material object ? Its simple issue but I dont know how to handle it properly.

  • I am not sure if this type of validation should be part of the domain object or not. And if not, is it possible to place it inside application layer or is it better to incorporate some domain service somehow ?

  • The solution which I lean, is to create IsMaterialValid(int number) as database procedure and call it from repository in application layer, after that validation, create material object and pass it to Order object. It seems like most direct and easy way. What I don't like is that I will outsource the domain validation into database procedure (but as a plus I will still be dependent on validation method defined in repository interface).

Any idea ?

  • Not sure which language you are working with, but a more narrow exception would be appropriate. InvalidOperationException suggests that the function is not available--or at least not in a state where it can take calls. A ValidationException or InvalidParameterException might be more informative. – Berin Loritsch Feb 4 at 16:55
  • Thank you Berin, its C# I use this exception as a default exception for all the business exceptions. – Muflix Feb 4 at 17:07
  • 1
    It really does matter where you intend to do the validation. If the validation is static (i.e. does not depend on data), then there is no problem with it in your domain object. However, if you need to check the database first, I'd recommend putting that validation inside the repository. That way your MaterialRepository can check for the existence of the requested material attributes either as a separate method, or as part of the save function. – Berin Loritsch Feb 4 at 17:21
  • I'm a little confused about the meaning of the "Detail" method. What business function is it modeling? A "detail" is usually a noun - a person, place or thing. Are you using it as a verb, as in "to detail a car"? – Greg Burghardt Feb 4 at 18:30
  • I'm also not sure what materialCondition, materialStatus and materialCondition have to do this any particular instance of Material. They are arguments passed in to the "Detail" method that constrain the values of properties on Matrial. Why are those three arguments not members of Material? Is this partial code? – Greg Burghardt Feb 4 at 18:31
0

Before we discuss the proper way to model your problem, we need to remind ourselves that DDD is about focusing on the behavior our system is to exhibit. Unfortunately your question above takes a very data-centric perspective, and as a result makes synthesizing a solution more difficult for all parties involved. That is, it is still unclear to me what you are trying to do (other than avoid an exception). Of course I can glean that you don't want a Material added to an Order if it doesn't meet some criteria, but you never actually, specifically, denote that. I know what your thinking, "I have so!". Let me explain.

There are two ways to model this problem according to DDD. The first option asks us to determine what we are trying to do and incorporate the business rules in that process (this is where my first point above is relevant). If we are following DDD and modeling the behavior of our system, then we should be modeling our business rules with that behavior. So if our behavior is "we can only Add a Material to an Order if it meets certain criteria", it would then follow that the logic to enforce our invariant should be placed in our Order.AddMaterial method! Option 1 is to refactor the aforementioned method to something like:

AddMaterial(Material material, int reqId, int qty, string type, string status, string 
         condition)

Or possibly (if we want to prevent the caller from holding a reference):

AddMaterial(int num, int reqId, int qty, string type, string status, string condition)

and place all of the those checks in this method. This makes sense right? It isn't the construction of a Material that needs extra checks. Rather, we only need to make sure a Material meets our requirement when it is to be added to an Order.

As an aside, an object's constructor should almost never contain business rules (there are exceptions for certain kinds of value objects where the object itself represents an abstraction with well-known, verifiable, semantics. e.g. EmailAddress ). It is data-centric thinking that leads to a failure to properly model, and often even understand, behavior. This usually manifests in the creation of abominations for constructors.

The second option is to move this logic into a Repository. Importantly, not in the way you have described! There is no such thing as (public) isValid-type methods when following DDD. Again, this is because it starts to move our focus away from the behavior of our system, and instead towards the data. A more DDD-centric approach is to model this invariant as a query that could return nothing (or throw):

// Order Command Handler

// throws if not found (respecting invariants)
var material = materials.Find(cmd.materialNumber);

order.AddMaterial(material, cmd.requestId, cmd.quantity);

In this way we are able to both leverage our database to help with validation and also keep our focus on the behavior of our system. That is, we keep validation "under the hood".

Note: There are suitable variations of the above approaches for differing "slices"/definitions of Material. The versions I have chosen in my examples above are actually distinct from one another and are both different from the version you give above. It may be more instructive if I named your version MaterialItem, my version for option 1 simply Material, and my version for option 2 OrderMaterial.

This is because your version seems to include semantics that only the Order would care about (e.g. quantity), my option 1 version represents only the thing itself (combined with Order semantics when added), and my option 2 is a version that does not include Order semantics but does represent a material that is suitable to be added to an Order.

What I am saying is that the differing contexts (read: behaviors) within which we are operating affect how we understand our model.

| improve this answer | |
  • Thank you for your answer, placing validation into Order make sense but the validation for the material number is common for the whole domain context, so I don't think it's necessary to add the validation to the Order. In my case I believe it is not directly the Orders responsibility. So therefore I like your second option where you hide the validation in the material repository. – Muflix Feb 11 at 0:51
  • When I am thinking about it again, my object naming is confusing, becase Material is not really a material, but more likely an OrderItem which contains an identifier of material which I want to validate. But anyway, in application service I need to use that var material = materials.Find(cmd.materialNumber); to receive a material and not create it directly via constructor, otherwise it will evade the validation which is very similar as using materials.IsMaterialNumberValid(cmd.materialNumber); only naming convention is different. – Muflix Feb 11 at 0:51
  • What I dont understand is why constructor should almost never contains business rules ? I need to ensure, that domain entity is always valid, even when created. – Muflix Feb 11 at 0:53
  • 1
    @Muflix Coincidentally I already have an answer to this one: softwareengineering.stackexchange.com/questions/372338/… – king-side-slide Feb 11 at 15:33
  • Good answer @king-side-slide, I agree with the example that validations such as "Customer must be over 18 to register", "Customer must be under 25 to qualify for discount on registration", "Customer must be over 25 to make reservation" should be part of domain methods. But some general validations in constructor doesn't seem like a problem to me (based on my limited experience), in other way I can create some factory method for the object creation which is technical workaround with nicer name, if needed. – Muflix Feb 11 at 16:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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