Ever since my revelation about "valid" domain objects, my mind has been mulling over hot, steamy thoughts on data validation. If we think of domain objects as "Data Types," then they have to do some minimal amount internal validation just to make sure that they have the required information to exist in a valid state. For example, an Account object might check within its constructor to see that its assigned AccountNumber has a length because it wouldn't make sense for an Account to exist without an AccountNumber. It wouldn't check, however, to see if the AccountNumber was valid within the greater system as it might not know what those rules are.
So this got me thinking about the code that instantiates an Account object. Because the Account object does some critical data validation, we have to remember that it will throw an exception if passed-in parameters are not valid. This is both the required and the expected behavior - like trying to instantiate an INT data type with a STRING value - it simply can't be done. As such, in order to prevent exceptions, the code that instantiates an Account object has to also do validation on the Account parameters to make sure that they won't be invalid for Account creation.
Therefore, to handle Account creation gracefully, we have both the calling code and the Account object constructor checking to see if AccountNumber has a length. At first, I thought this was a duplication of logic; I wondered how I could factor this logic check out into a single place. But then, I realized that there wasn't really any redundancy going on. Yes, the same checks are being performed in two different places, but the intent of those checks is different, and I believe that this means that the logic is not duplicated.
To explain further, the logic check within the Account constructor is done with the intent to make sure that the data type "Account" is valid. The logic check within the calling code, on the other hand, is done with the intent to make sure that the data provided can be used to create an Account. While the difference might seem subtle, it's actually quite large. The calling code does not want to throw an exception because it is most likely part of a larger, user-driven work flow. The Account, on the other hand, couldn't care less about work flow or whether or not a raised exception would be a bad idea. Furthermore, the Account data type might be used in many situations within a single applications and cannot depend on any particular calling code to exist. For example, you might be creating Accounts based on a database query. The calling code might expect the database query to contain only valid data and therefore does not do any preliminary validation. But, the Accounts it tries to create based on that query data still have to perform critical validation as we can't have Accounts objects being created if the database data is corrupted.
All to say, seemingly redundant data validation in an object oriented work flow is not really redundant as the intent behind the validation is different depending on where it is being performed.