Home > Software design >  DDD - Life cycle of Value Objects: Validation & Persistence
DDD - Life cycle of Value Objects: Validation & Persistence

Time:10-17

I understand what is VO (immutable, no-identity, ...). But I have several questions that are from discussion with my co-workers.

A) Validation - How precise should it be?

What types of validation should I put into VO? Only basic ones or all? Good example of VO is Email with some regexp validation. I've seen it many times. I've worked on several big/medium-size applications and regexp wasn't good enough because:

  • system-A: Domain name of email was validated, eg [email protected] in invalid email because domain gmali.com doesn't exist
  • system-B: We had list (service) of banned domains of "temporary email services" because we wanted to avoid of "fake accounts"

I cannot imagine to put validation of this kind into VO, because it require network communication and VO will be complicated (and slow).

B) Validation: Names, Titles, Strings... is length part of VO?

Programmer can use old-good string data type. I can image VO as NotEmptyString, but is it good approach to create value objects as:

  • FirstName (non-empty string with length limitation)
  • Surname (non-empty string with length limitation)
  • StreetName(non-empty string with length limitation)

There is no difference between FirstName and Surname, because in application we cannot find out if some one swap first name and surname in form. Robert can be first name and it can be also surname...

class Person 
{
  private string $firstName; // no VO for firstName
  // or 
  private FirstName $firstName;  // VO just for firstName & length validation
  // or
  private NotEmptyString $firstName; // VO - no max length validation
  // or
  private StringLength50 $firstName; // same as FirstName, different name for sharing 
}

Which approach is the best and why?

C) Copy of VO: Providing "Type-Safety" for entity arguments?

This point is similar to previous one.

Is it good practice to create classes like this:

class Surname extends Name
{

}

class FirstName extends Name
{

}

just "to alias" VO?

D) Persistence: Reading stored VO

This point is closely related to first one: A) Validation - How precise should it be?. I strongly believe what is stored in my "storage engine" (DB) is valid - no questions. I don't see any reason why I should validate VO again when everything was validated during "persistence step". Even complex/poorly-written regexp could be performance killer - listing of N-hundreds on user emails. I'm lost here... should I validate only basic stuff and use same VO during persist and read or should I have 2 separate VO for these cases?

E) Persistence/Admin: Something like "god" in the system.

From my experience: In real-word system user with higher privileges can sometimes by-pass validation rules and this is again related to point A) Example:

  • you (as regular user of system) can make online reservation up to 30 days from today
  • admin user can make online reservation to any date

Should I use only Date / FutureDate VO or what?

F) Persistence: Mapping to DB data-types

Is it good practice to closely bound VO and DB (storage engine) data types?

If FirstName can have only 50 chars should it be defined / mapped to VAR_CHAR(50)?

Thanks.

CodePudding user response:

A) Validation - How precise should it be?

It's not about precision, it's about invariants & responsibility. A value object (VO) can't possibly have authority on whether or not an email address exists. That's a fact that varies and can't be controlled by the VO. Even if you had code such as the following:

var emailAddress = EmailAddress.of('[email protected]', emailValidityChecker);

The address may not exist a few minutes later, the user may have lost his account password forever, etc.

So what does EmailAddress should represent? It should ensure the "format" of the address makes it a usable & useful address in your domain.

For instance, in a system responsible for delivering tax reminders, I had a limitation where I had to use Exchange and it couldn't support certain email formats like addresses with "leading, trailing or consecutive dots in the local-part" (took the exact comment I had put).

Even though that's a technical concern in theory, that means our system couldn't ingest such email addresses and they were completely useless to us so the ValidEmailAddress VO did not accept those to fail early (otherwise it was generating false positives down the chain).

B) Validation: Names, Titles, Strings... is length part of VO?

I would, even though such lengths might sometimes feel somewhat arbitrary or infrastructure-driven. However, I think it's safe to say that a name with 500 characters is certainly a mistake. Furthermore, validating with reasonable ranges can protect against attacks (e.g. a 1GB name???). Some may argue that it's purely an infrastructure concern and would put the validation at another layer, but I disagree and I think the distinction is unhelpful.

The length rules aren't always arbitrary, for instance a TweetMessage that can't be longer than 280 chars, that's a domain rule.

Does that mean you must have a VO for every possible strings in the system? Honestly I pushed backed being scared to overuse VOs and edge towards a VO-obsession rather than primitive obsession, but in almost every scenario I wished I just took the time to wrap that damn string.

Be pragmatic, but I see more harm in underusing than overusing VOs.

C) Copy of VO: Providing "Type-Safety" for entity arguments?

I most likely wouldn't extend Name just for the sake of reuse here. There's most likely no place where you'd want to interchange a Surename with a FirstName so polymorphism is pretty useless too. However, the explicit types may help to interchange "surename" for "first name" and vice-versa.

Independently of whether or not the explicit types are useful, something more useful here might be to aggregate both under a FullName VO that creates increases cohesion.

Please beware that overly restrictive name policies has been a huge pain point for many international systems though...

D) Persistence: Reading stored VO

Persisted data lives on the "safe" side and should NOT be validated again when loaded into memory. You should be able to circumvent the validation path when hydrating our VOs.

E) Persistence/Admin: Something like "god" in the system.

VOs are great to enforce their "invariants". An invariant by definition doesn't vary given the context. That's actually something many misunderstood when saying "always-valid" approach doesn't work.

That said, even system admins most likely can't make new reservations in the past, so perhaps that can be an invariant of a ReservationDate. Ultimately you would most likely extract the other rules in the context to which they belong.

F) Persistence: Mapping to DB data-types

I think it's more important to reflect the DB limitation in the domain than inversely, reflect the domain limitation in the DB. If your DB only accepts 50 chars and you exceed that some systems will just crash with a very cryptic error message not even telling you which column overflowed. Validating in the domain would help debugging much more quickly. However, you do not necessarily have to match the domain rule in the DB.

CodePudding user response:

DDD, like any other design, is a matter of drawing lines and making abstract rules. Some rules may be very strict, while others may be fluent to some extent. The important thing is to keep consistency as much as possible, rather than striving to build the ultimate-undefeatable domain.

Validation - How precise should it be?

"Heavy" validations should not occur inside VO. A VO is not very different in its nature from the primitive it encapsulates, therefore validations should be independent of external factors. Please recall that even primitives such as byte may be internally validated: an exception (sometimes even a compile error) occurs when a byte variable is assigned with value greater than 255.

Advanced validations, on the other hand, belong to the flow part (use-case / interactor / command-handler), since they involve operations beyond the scope of the VO's primitive, such as querying databases or invoking APIs. You can, for example, query a list of valid email providers from database, check if VO's provider contained in list, and throw exception if not. This is simply flow.

You may, of course, decide to have an in-memory static list of email providers, in which case it will be perfectly valid to keep it inside VO and check its primitive against that list. No need to communicate with external world, everything is "local". Would it be scalable? probably not. But it follows a DDD rule stating that VO should not "speak" with external resources.

Validation: Names, Titles, Strings... is length part of VO?

VOs, much like other DDD concepts, should "speak out loud" your business domain, meaning that they should express business semantics. This is why FirstName, Surname and StreetName are good names, while NotEmptyString is less preferable due to the fact it communicates technical rather than business details.

If, for example, your business states that customers with a more-than-50-characters-length name are to be addressed differently than customers with a less-than-50-characters-length name, then you probably should have two VOs, e.g. LongFirstName, ShortFirstName.

True, several VOs may require exactly the same validations, e.g. both StreetName and CityName must start with a capital and length cannot exceed 100. Does this mean we have to make great effort to avoid duplications in the name of "reusability"? I would say no, especially if avoiding duplications means having a single VO named CapitalHeadStringUpTo100Characters. Such name conveys no business purpose. Moreover, if CityName suddenly requires additional validations, breaking CapitalHeadStringUpTo100Characters into two VOs may require much work.

Copy of VO: Providing "Type-Safety" for entity arguments?

Inheritance is a tool provided by development platform, it is more than OK to use it, but only to the point where things get messy or too abstract. Remember, VO only expresses a specific domain-approach principle. The polymorphism OOP principle, on the other hand, which of course may be applied in DDD applications, is tightly coupled with abstraction concepts (i.e. base classes), and I would say it should fit better to the entities model part.

BTW, you can find on web several implementations for a base VO class.

Persistence: Reading stored VO

System designs were to be of less importance if the same validations had occurred over and over again in different points of a single use case. Unless you have a reason to believe that your database can be altered by external components, it is sufficient to reconstitute an entity from database without re-validating. Also keep in mind that a typical entity may embed at least one VO, which is the same VO used both in "persistence step" (when entity is being constructed) and in "reading step" (when being reconstituted).

Persistence/Admin: Something like "god" in the system.

Multitenancy applications can be aware of multiple kinds of users. Software does not care if one user is more powerful than another one, it is only subjected to rules. Whether you choose to have a single general entity User with VO FutureDate allowed to be set with null, or two entities User, Admin with VOs FutureDate (not null), FutureDate (nullable) respectively, is less of our interest here. The important thing is that multitenancy can be achieved through smart usage of dependency injection: system identifies user privileges and infers what factories, services or validations are to be injected.

Persistence: Mapping to DB data-types

It really depends on level of maturity in the DDD field. Applications will always have bugs, and you should have some clue on your business's bug-tolerance level in case you choose to design a lenient database.

Aside of that, keep in mind that no matter how much effort you put into it, your database can probably never reflect the full set of business invariants: limiting a single VO to some length is easy, but setting rules involving multiple VOs (that is when one VO's validity depends another VO) is less convenient.

  • Related